Skip to content

Commit 2bd0f27

Browse files
feat: expose session context to model plugins via get_current_session()
1 parent 705ca0d commit 2bd0f27

5 files changed

Lines changed: 423 additions & 70 deletions

File tree

src/dify_plugin/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
monkey.patch_all(sys=True)
55

66
from dify_plugin.config.config import DifyPluginEnv
7+
from dify_plugin.core.session_context import get_current_session
78
from dify_plugin.interfaces.agent import AgentProvider, AgentStrategy
89
from dify_plugin.interfaces.endpoint import Endpoint
910
from dify_plugin.interfaces.model import ModelProvider
@@ -51,4 +52,5 @@
5152
"TextEmbeddingModel",
5253
"Tool",
5354
"ToolProvider",
55+
"get_current_session",
5456
]

src/dify_plugin/core/plugin_executor.py

Lines changed: 114 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
)
5252
from dify_plugin.core.plugin_registration import PluginRegistration
5353
from dify_plugin.core.runtime import Session
54+
from dify_plugin.core.session_context import _current_session
5455
from dify_plugin.core.utils.http_parser import deserialize_request, serialize_response
5556
from dify_plugin.entities import ParameterOption
5657
from dify_plugin.entities.agent import AgentRuntime
@@ -242,16 +243,31 @@ def invoke_llm(self, session: Session, data: ModelInvokeLLMRequest) -> object:
242243
data.model_type,
243244
)
244245
if isinstance(model_instance, LargeLanguageModel):
245-
return model_instance.invoke(
246-
data.model,
247-
data.credentials,
248-
data.prompt_messages,
249-
data.model_parameters,
250-
data.tools,
251-
data.stop,
252-
data.stream,
253-
data.user_id,
254-
)
246+
token = _current_session.set(session)
247+
try:
248+
result = model_instance.invoke(
249+
data.model,
250+
data.credentials,
251+
data.prompt_messages,
252+
data.model_parameters,
253+
data.tools,
254+
data.stop,
255+
data.stream,
256+
data.user_id,
257+
)
258+
except Exception:
259+
_current_session.reset(token)
260+
raise
261+
262+
# invoke() returns a Generator (lazy). Keep ContextVar set
263+
# until fully consumed — _invoke() needs session access.
264+
def _with_session_context(gen: Generator) -> Generator:
265+
try:
266+
yield from gen
267+
finally:
268+
_current_session.reset(token)
269+
270+
return _with_session_context(result)
255271
msg = f"Model `{data.model_type}` not found for provider `{data.provider}`"
256272
raise ValueError(
257273
msg,
@@ -291,12 +307,16 @@ def invoke_text_embedding(
291307
data.model_type,
292308
)
293309
if isinstance(model_instance, TextEmbeddingModel):
294-
return model_instance.invoke(
295-
data.model,
296-
data.credentials,
297-
data.texts,
298-
data.user_id,
299-
)
310+
token = _current_session.set(session)
311+
try:
312+
return model_instance.invoke(
313+
data.model,
314+
data.credentials,
315+
data.texts,
316+
data.user_id,
317+
)
318+
finally:
319+
_current_session.reset(token)
300320
msg = f"Model `{data.model_type}` not found for provider `{data.provider}`"
301321
raise ValueError(
302322
msg,
@@ -312,13 +332,17 @@ def invoke_multimodal_embedding(
312332
data.model_type,
313333
)
314334
if isinstance(model_instance, TextEmbeddingModel):
315-
return model_instance.invoke_multimodal(
316-
data.model,
317-
data.credentials,
318-
data.documents,
319-
user=data.user_id,
320-
input_type=data.input_type,
321-
)
335+
token = _current_session.set(session)
336+
try:
337+
return model_instance.invoke_multimodal(
338+
data.model,
339+
data.credentials,
340+
data.documents,
341+
user=data.user_id,
342+
input_type=data.input_type,
343+
)
344+
finally:
345+
_current_session.reset(token)
322346
msg = f"Model `{data.model_type}` not found for provider `{data.provider}`"
323347
raise ValueError(
324348
msg,
@@ -352,15 +376,19 @@ def invoke_rerank(self, session: Session, data: ModelInvokeRerankRequest) -> obj
352376
data.model_type,
353377
)
354378
if isinstance(model_instance, RerankModel):
355-
return model_instance.invoke(
356-
data.model,
357-
data.credentials,
358-
data.query,
359-
data.docs,
360-
data.score_threshold,
361-
data.top_n,
362-
data.user_id,
363-
)
379+
token = _current_session.set(session)
380+
try:
381+
return model_instance.invoke(
382+
data.model,
383+
data.credentials,
384+
data.query,
385+
data.docs,
386+
data.score_threshold,
387+
data.top_n,
388+
data.user_id,
389+
)
390+
finally:
391+
_current_session.reset(token)
364392
msg = f"Model `{data.model_type}` not found for provider `{data.provider}`"
365393
raise ValueError(
366394
msg,
@@ -376,15 +404,19 @@ def invoke_multimodal_rerank(
376404
data.model_type,
377405
)
378406
if isinstance(model_instance, RerankModel):
379-
return model_instance.invoke_multimodal(
380-
data.model,
381-
data.credentials,
382-
data.query,
383-
data.docs,
384-
score_threshold=data.score_threshold,
385-
top_n=data.top_n,
386-
user=data.user_id,
387-
)
407+
token = _current_session.set(session)
408+
try:
409+
return model_instance.invoke_multimodal(
410+
data.model,
411+
data.credentials,
412+
data.query,
413+
data.docs,
414+
score_threshold=data.score_threshold,
415+
top_n=data.top_n,
416+
user=data.user_id,
417+
)
418+
finally:
419+
_current_session.reset(token)
388420
msg = f"Model `{data.model_type}` not found for provider `{data.provider}`"
389421
raise ValueError(
390422
msg,
@@ -400,20 +432,24 @@ def invoke_tts(
400432
data.model_type,
401433
)
402434
if isinstance(model_instance, TTSModel):
403-
b = model_instance.invoke(
404-
data.model,
405-
data.tenant_id,
406-
data.credentials,
407-
data.content_text,
408-
data.voice,
409-
data.user_id,
410-
)
411-
if isinstance(b, bytes | bytearray | memoryview):
412-
yield {"result": binascii.hexlify(b).decode()}
413-
return
435+
token = _current_session.set(session)
436+
try:
437+
b = model_instance.invoke(
438+
data.model,
439+
data.tenant_id,
440+
data.credentials,
441+
data.content_text,
442+
data.voice,
443+
data.user_id,
444+
)
445+
if isinstance(b, bytes | bytearray | memoryview):
446+
yield {"result": binascii.hexlify(b).decode()}
447+
return
414448

415-
for chunk in b:
416-
yield {"result": binascii.hexlify(chunk).decode()}
449+
for chunk in b:
450+
yield {"result": binascii.hexlify(chunk).decode()}
451+
finally:
452+
_current_session.reset(token)
417453
else:
418454
msg = f"Model `{data.model_type}` not found for provider `{data.provider}`"
419455
raise ValueError(
@@ -458,14 +494,18 @@ def invoke_speech_to_text(
458494

459495
with pathlib.Path(temp.name).open("rb") as f:
460496
if isinstance(model_instance, Speech2TextModel):
461-
return {
462-
"result": model_instance.invoke(
463-
data.model,
464-
data.credentials,
465-
f,
466-
data.user_id,
467-
),
468-
}
497+
token = _current_session.set(session)
498+
try:
499+
return {
500+
"result": model_instance.invoke(
501+
data.model,
502+
data.credentials,
503+
f,
504+
data.user_id,
505+
),
506+
}
507+
finally:
508+
_current_session.reset(token)
469509
msg = (
470510
f"Model `{data.model_type}` not found for provider "
471511
f"`{data.provider}`"
@@ -506,14 +546,18 @@ def invoke_moderation(
506546
)
507547

508548
if isinstance(model_instance, ModerationModel):
509-
return {
510-
"result": model_instance.invoke(
511-
data.model,
512-
data.credentials,
513-
data.text,
514-
data.user_id,
515-
),
516-
}
549+
token = _current_session.set(session)
550+
try:
551+
return {
552+
"result": model_instance.invoke(
553+
data.model,
554+
data.credentials,
555+
data.text,
556+
data.user_id,
557+
),
558+
}
559+
finally:
560+
_current_session.reset(token)
517561
msg = f"Model `{data.model_type}` not found for provider `{data.provider}`"
518562
raise ValueError(
519563
msg,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""
2+
Request-scoped session context for model plugins.
3+
4+
Model plugins (LLM, Embedding, Rerank, etc.) do not receive the Session
5+
object through their ``_invoke()`` signature — unlike tool plugins which
6+
get it via their constructor. This module bridges that gap by storing
7+
the current Session in a :class:`~contextvars.ContextVar` so that model
8+
plugin code can retrieve it on demand via :func:`get_current_session`.
9+
10+
Usage in a custom model plugin::
11+
12+
from dify_plugin.core.session_context import get_current_session
13+
14+
class MyLLM(LargeLanguageModel):
15+
def _invoke(self, model, credentials, prompt_messages, ...):
16+
session = get_current_session()
17+
if session and session.app_id:
18+
# tag the request with the originating Dify app
19+
...
20+
21+
Note on ``app_id`` being ``None``:
22+
23+
``session.app_id`` is ``None`` when the model is invoked outside of
24+
an app execution context — for example, RAG routing, conversation
25+
title generation, or suggested question generation. These calls
26+
represent shared infrastructure costs not attributable to a specific
27+
app.
28+
29+
When building provider-side cost dashboards, the recommended
30+
approach is:
31+
32+
* If ``app_id`` is not ``None``, tag the request with it for
33+
per-app cost attribution.
34+
* If ``app_id`` is ``None``, either skip tagging or use a
35+
sentinel value such as ``"dify_system"`` to bucket these
36+
calls separately from external (non-Dify) traffic.
37+
"""
38+
39+
from __future__ import annotations
40+
41+
from contextvars import ContextVar
42+
from typing import TYPE_CHECKING
43+
44+
if TYPE_CHECKING:
45+
from dify_plugin.core.runtime import Session
46+
47+
_current_session: ContextVar[Session | None] = ContextVar(
48+
"_current_session", default=None
49+
)
50+
51+
52+
def get_current_session() -> Session | None:
53+
"""Return the :class:`Session` for the current model invocation, or
54+
``None`` when called outside of a plugin dispatch context.
55+
56+
Returns:
57+
The current session, or ``None``.
58+
"""
59+
return _current_session.get()

tests/core/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)