Skip to content

feat(api): pass app_id to model plugins for provider-side cost attribution#35859

Merged
WH-2099 merged 4 commits into
langgenius:mainfrom
ryuta-kobayashi-ug:feat/pass-app-id-to-model-plugins
Jul 6, 2026
Merged

feat(api): pass app_id to model plugins for provider-side cost attribution#35859
WH-2099 merged 4 commits into
langgenius:mainfrom
ryuta-kobayashi-ug:feat/pass-app-id-to-model-plugins

Conversation

@ryuta-kobayashi-ug

Copy link
Copy Markdown
Contributor

Fixes #35772

Summary

Pass app_id to model plugins so they can tag LLM requests with the originating Dify app for provider-side cost attribution (e.g., Bedrock requestMetadata, OpenAI metadata, Vertex AI labels).

Changes

  • Add app_context.py with ContextVar-based get_current_app_id() / set_current_app_id()
  • Set app_id in all app runners (chat, completion, agent_chat, advanced_chat, workflow) before LLM invocation
  • PluginModelRuntime.invoke_llm() reads get_current_app_id() and passes it to PluginModelClient
  • PluginModelClient includes app_id in the HTTP payload to plugin daemon
  • app_id is None when called outside app context (RAG routing, title generation, etc.)

Design

Uses contextvars.ContextVar because PluginModelRuntime is created at tenant scope (before app execution), and graphon sits between the app runner and PluginModelRuntime with a fixed interface that cannot pass app_id as an argument.

Testing

  • Unit tests for _dispatch_payload with/without app_id
  • E2E tested all 5 app types (chatbot, chatflow, workflow, text generator, agent) — all pass app_id correctly
  • Regression tested: official plugin, RAG, blocking API mode — no degradation

Checklist

  • This change requires a documentation update
  • I've added a test for each change that was introduced
  • I ran dev/reformat(backend) to appease the lint gods

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label May 6, 2026
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Diff

base → PR
--- /tmp/pyrefly_base.txt	2026-07-06 08:41:05.066004883 +0000
+++ /tmp/pyrefly_pr.txt	2026-07-06 08:40:52.730022451 +0000
@@ -2521,79 +2521,81 @@
 ERROR Class member `DummyRunner._organize_prompt_messages` overrides a member in a parent class but is missing an `@override` decorator [missing-override-decorator]
   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:24:9
 ERROR `None` is not assignable to attribute `agent_callback` with type `DifyAgentCallbackHandler` [bad-assignment]
-  --> tests/unit_tests/core/agent/test_cot_agent_runner.py:80:29
+  --> tests/unit_tests/core/agent/test_cot_agent_runner.py:81:29
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-  --> tests/unit_tests/core/agent/test_cot_agent_runner.py:83:5
+  --> tests/unit_tests/core/agent/test_cot_agent_runner.py:84:5
 ERROR Missing required key `action` for TypedDict `ActionDict` [bad-typed-dict-key]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:113:44
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:114:44
 ERROR Missing required key `action_input` for TypedDict `ActionDict` [bad-typed-dict-key]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:113:44
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:114:44
 ERROR Key `invalid` is not defined in TypedDict `ActionDict` [bad-typed-dict-key]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:113:45
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:114:45
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:167:55
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:168:55
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:180:55
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:181:55
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:204:35
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:205:35
 ERROR `None` is not assignable to attribute `agent_callback` with type `DifyAgentCallbackHandler` [bad-assignment]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:223:33
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:224:33
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:226:29
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:227:29
 ERROR Object of class `NoneType` has no attribute `max_iteration` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:229:9
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:230:9
 ERROR `None` is not assignable to attribute `agent_callback` with type `DifyAgentCallbackHandler` [bad-assignment]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:245:33
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:246:33
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:248:29
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:249:29
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:259:35
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:260:35
 ERROR Object of class `NoneType` has no attribute `max_iteration` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:263:9
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:264:9
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:275:29
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:276:29
 ERROR Object of class `NoneType` has no attribute `max_iteration` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:280:9
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:281:9
 ERROR `Literal[1]` is not assignable to attribute `prompt_price` with type `Decimal` [bad-assignment]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:286:32
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:287:32
 ERROR `Literal[1]` is not assignable to attribute `completion_price` with type `Decimal` [bad-assignment]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:287:36
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:288:36
 ERROR `Literal[2]` is not assignable to attribute `total_price` with type `Decimal` [bad-assignment]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:288:31
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:289:31
 ERROR `Literal[1]` is not assignable to attribute `prompt_price` with type `Decimal` [bad-assignment]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:294:32
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:295:32
 ERROR `Literal[1]` is not assignable to attribute `completion_price` with type `Decimal` [bad-assignment]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:295:36
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:296:36
 ERROR `Literal[2]` is not assignable to attribute `total_price` with type `Decimal` [bad-assignment]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:296:31
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:297:31
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:324:35
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:325:35
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:343:35
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:344:35
+ERROR Object of class `FunctionType` has no attribute `call_args` [missing-attribute]
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:345:16
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:357:25
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:359:25
 ERROR Object of class `NoneType` has no attribute `max_iteration` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:377:9
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:379:9
 ERROR `None` is not assignable to attribute `agent_callback` with type `DifyAgentCallbackHandler` [bad-assignment]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:385:33
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:387:33
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:387:25
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:389:25
 ERROR `list[str]` is not assignable to attribute `_agent_scratchpad` with type `list[AgentScratchpadUnit]` [bad-assignment]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:417:36
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:419:36
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:439:55
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:441:55
 ERROR Object of class `FunctionType` has no attribute `assert_called` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:443:9
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:445:9
 ERROR Class member `BadValue.__str__` overrides a member in a parent class but is missing an `@override` decorator [missing-override-decorator]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:453:17
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:455:17
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:509:35
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:511:35
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:523:35
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:525:35
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:537:35
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:539:35
 ERROR Object of class `DummyRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:552:35
+   --> tests/unit_tests/core/agent/test_cot_agent_runner.py:554:35
 ERROR `list[DummyTool]` is not assignable to attribute `_prompt_messages_tools` with type `Sequence[PromptMessageTool]` [bad-assignment]
   --> tests/unit_tests/core/agent/test_cot_chat_agent_runner.py:49:37
 ERROR `DummyGenerateEntity` is not assignable to attribute `application_generate_entity` with type `AgentChatAppGenerateEntity` [bad-assignment]
@@ -2617,53 +2619,55 @@
 ERROR `Literal[0]` is not assignable to attribute `total_price` with type `Decimal` [bad-assignment]
   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:32:25
 ERROR Missing argument `session` in function `core.agent.base_agent_runner.BaseAgentRunner.__init__` [missing-argument]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:102:37
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:103:37
 ERROR Object of class `FunctionCallAgentRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:133:5
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:134:5
 ERROR Argument `DummyChunk` is not assignable to parameter `llm_result_chunk` with type `LLMResultChunk` in function `core.agent.fc_agent_runner.FunctionCallAgentRunner.check_tool_calls` [bad-argument-type]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:153:40
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:154:40
 ERROR Argument `DummyResult` is not assignable to parameter `llm_result` with type `LLMResult` in function `core.agent.fc_agent_runner.FunctionCallAgentRunner.check_blocking_tool_calls` [bad-argument-type]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:158:49
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:159:49
 ERROR Argument `DummyChunk` is not assignable to parameter `llm_result_chunk` with type `LLMResultChunk` in function `core.agent.fc_agent_runner.FunctionCallAgentRunner.extract_tool_calls` [bad-argument-type]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:174:43
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:175:43
 ERROR Argument `DummyChunk` is not assignable to parameter `llm_result_chunk` with type `LLMResultChunk` in function `core.agent.fc_agent_runner.FunctionCallAgentRunner.extract_tool_calls` [bad-argument-type]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:185:43
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:186:43
 ERROR Argument `DummyResult` is not assignable to parameter `llm_result` with type `LLMResult` in function `core.agent.fc_agent_runner.FunctionCallAgentRunner.extract_blocking_tool_calls` [bad-argument-type]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:196:52
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:197:52
 ERROR Argument `None` is not assignable to parameter `query` with type `str` in function `core.agent.fc_agent_runner.FunctionCallAgentRunner._organize_user_query` [bad-argument-type]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:232:46
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:233:46
 ERROR Object of class `FunctionCallAgentRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:300:35
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:301:35
+ERROR Object of class `FunctionType` has no attribute `call_args` [missing-attribute]
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:303:16
 ERROR Object of class `FunctionType` has no attribute `assert_called` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:302:9
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:304:9
 ERROR Object of class `FunctionType` has no attribute `call_args_list` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:304:23
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:306:23
 ERROR Argument `list[TextPromptMessageContent]` is not assignable to parameter `content` with type `str | None` in function `DummyMessage.__init__` [bad-argument-type]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:312:57
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:314:57
 ERROR Object of class `FunctionCallAgentRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:319:35
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:321:35
 ERROR Argument `list[TextPromptMessageContent]` is not assignable to parameter `content` with type `str | None` in function `DummyMessage.__init__` [bad-argument-type]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:332:57
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:334:57
 ERROR Object of class `FunctionCallAgentRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:342:35
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:344:35
 ERROR Argument `list[TextPromptMessageContent]` is not assignable to parameter `content` with type `str | None` in function `DummyMessage.__init__` [bad-argument-type]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:348:46
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:350:46
 ERROR Object of class `FunctionCallAgentRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:353:35
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:355:35
 ERROR Object of class `FunctionType` has no attribute `call_args` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:355:16
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:357:16
 ERROR Object of class `FunctionCallAgentRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:382:35
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:384:35
 ERROR Object of class `FunctionCallAgentRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:400:35
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:402:35
 ERROR Object of class `FunctionCallAgentRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:429:35
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:431:35
 ERROR Object of class `FunctionType` has no attribute `call_args_list` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:435:25
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:437:25
 ERROR Object of class `NoneType` has no attribute `max_iteration` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:439:9
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:441:9
 ERROR Object of class `FunctionCallAgentRunner` has no attribute `session` [missing-attribute]
-   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:454:29
+   --> tests/unit_tests/core/agent/test_fc_agent_runner.py:456:29
 ERROR Missing argument `description` in function `core.agent.plugin_entities.AgentStrategyEntity.__init__` [missing-argument]
    --> tests/unit_tests/core/agent/test_plugin_entities.py:281:32
 ERROR Invalid key for TypedDict `AppParametersDict`, got `Unknown` [bad-typed-dict-key]
@@ -4571,33 +4575,33 @@
 ERROR Argument `None` is not assignable to parameter `project` with type `str` in function `core.ops.utils.validate_project_name` [bad-argument-type]
    --> tests/unit_tests/core/ops/test_utils.py:136:40
 ERROR Missing required key `content` for TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:452:19
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:472:19
 ERROR Missing required key `content_type` for TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:452:19
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:472:19
 ERROR Key `type` is not defined in TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:452:20
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:472:20
 ERROR Key `value` is not defined in TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:452:36
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:472:36
 ERROR Missing required key `content` for TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:453:19
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:473:19
 ERROR Missing required key `content_type` for TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:453:19
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:473:19
 ERROR Key `type` is not defined in TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:453:20
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:473:20
 ERROR Key `value` is not defined in TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:453:37
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:473:37
 ERROR Missing required key `content` for TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:472:17
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:492:17
 ERROR Missing required key `content_type` for TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:472:17
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:492:17
 ERROR Key `type` is not defined in TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:472:18
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:492:18
 ERROR Missing required key `content` for TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:473:18
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:493:18
 ERROR Missing required key `content_type` for TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:473:18
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:493:18
 ERROR Key `type` is not defined in TypedDict `MultimodalRerankInput` [bad-typed-dict-key]
-   --> tests/unit_tests/core/plugin/impl/test_model_client.py:473:19
+   --> tests/unit_tests/core/plugin/impl/test_model_client.py:493:19
 ERROR Argument `werkzeug.wrappers.request.Request` is not assignable to parameter `request` with type `flask.wrappers.Request` in function `core.plugin.impl.trigger.PluginTriggerClient.invoke_trigger_event` [bad-argument-type]
    --> tests/unit_tests/core/plugin/impl/test_trigger_client.py:127:21
 ERROR Argument `werkzeug.wrappers.request.Request` is not assignable to parameter `request` with type `flask.wrappers.Request` in function `core.plugin.impl.trigger.PluginTriggerClient.invoke_trigger_event` [bad-argument-type]
@@ -6581,15 +6585,15 @@
 ERROR `SimpleNamespace` is not assignable to attribute `_dify_context` with type `DifyRunContext` [bad-assignment]
    --> tests/unit_tests/core/workflow/test_node_factory.py:487:33
 ERROR Argument `SimpleNamespace` is not assignable to parameter `node_data` with type `LLMNodeData | ParameterExtractorNodeData | QuestionClassifierNodeData` in function `core.workflow.node_factory.DifyNodeFactory._wrap_model_instance_for_node` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_factory.py:780:23
+   --> tests/unit_tests/core/workflow/test_node_factory.py:781:23
 ERROR Argument `_ModelInstanceStub` is not assignable to parameter `model_instance` with type `ModelInstance` in function `core.workflow.node_factory.DifyNodeFactory._wrap_model_instance_for_node` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_factory.py:781:28
+   --> tests/unit_tests/core/workflow/test_node_factory.py:782:28
 ERROR `SimpleNamespace` is not assignable to attribute `graph_runtime_state` with type `GraphRuntimeState` [bad-assignment]
-   --> tests/unit_tests/core/workflow/test_node_factory.py:998:39
+   --> tests/unit_tests/core/workflow/test_node_factory.py:999:39
 ERROR `SimpleNamespace` is not assignable to attribute `_dify_context` with type `DifyRunContext` [bad-assignment]
-    --> tests/unit_tests/core/workflow/test_node_factory.py:1128:33
+    --> tests/unit_tests/core/workflow/test_node_factory.py:1129:33
 ERROR `SimpleNamespace` is not assignable to attribute `graph_runtime_state` with type `GraphRuntimeState` [bad-assignment]
-    --> tests/unit_tests/core/workflow/test_node_factory.py:1129:39
+    --> tests/unit_tests/core/workflow/test_node_factory.py:1130:39
 ERROR `object | None` is not assignable to attribute `model_runtime` with type `LLMModelRuntime` [bad-assignment]
   --> tests/unit_tests/core/workflow/test_node_runtime.py:68:30
 ERROR Class member `_ModelTypeInstanceStub.get_model_schema` overrides parent class `LargeLanguageModel` in an inconsistent manner [bad-override-param-name]
@@ -6603,41 +6607,41 @@
 ERROR Object of class `FunctionType` has no attribute `assert_called_once_with` [missing-attribute]
    --> tests/unit_tests/core/workflow/test_node_runtime.py:193:5
 ERROR Argument `_ModelInstanceStub` is not assignable to parameter `model_instance` with type `ModelInstance` in function `core.workflow.node_runtime.DifyPreparedLLM.__init__` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:206:32
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:207:32
 ERROR Argument `_ModelInstanceStub` is not assignable to parameter `model_instance` with type `ModelInstance` in function `core.workflow.node_runtime.DifyPreparedLLM.__init__` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:214:32
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:215:32
 ERROR Argument `SimpleNamespace` is not assignable to parameter `model_instance` with type `ModelInstance` in function `core.workflow.node_runtime.DifyPreparedLLM.__init__` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:240:32
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:241:32
 ERROR Argument `_ModelInstanceStub` is not assignable to parameter `model_instance` with type `ModelInstance` in function `core.workflow.node_runtime.DifyPreparedPollingLLM.__init__` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:265:39
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:266:39
 ERROR Argument `_ModelInstanceStub` is not assignable to parameter `model_instance` with type `ModelInstance` in function `core.workflow.node_runtime.DifyPreparedPollingLLM.__init__` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:322:39
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:323:39
 ERROR Argument `Literal['chat']` is not assignable to parameter `model_mode` with type `LLMMode` in function `core.workflow.node_runtime.DifyPromptMessageSerializer.serialize` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:339:20
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:340:20
 ERROR Argument `SimpleNamespace` is not assignable to parameter `file_reference_factory` with type `FileReferenceFactoryProtocol` in function `core.workflow.node_runtime.DifyRetrieverAttachmentLoader.__init__` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:374:32
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:375:32
 ERROR Object of class `NoneType` has no attribute `storage_key` [missing-attribute]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:385:12
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:386:12
 ERROR Argument `SimpleNamespace` is not assignable to parameter `file_reference_factory` with type `FileReferenceFactoryProtocol` in function `core.workflow.node_runtime.DifyRetrieverAttachmentLoader.__init__` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:450:32
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:451:32
 ERROR Argument `SimpleNamespace` is not assignable to parameter `file_reference_factory` with type `FileReferenceFactoryProtocol` in function `core.workflow.node_runtime.DifyRetrieverAttachmentLoader.__init__` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:476:32
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:477:32
 ERROR Argument `object` is not assignable to parameter `variable_pool` with type `VariablePool | None` in function `core.workflow.node_runtime.DifyToolNodeRuntime.get_runtime` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:574:23
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:575:23
 ERROR Object of class `object` has no attribute `tool` [missing-attribute]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:578:12
-ERROR Object of class `object` has no attribute `parent_trace_context` [missing-attribute]
    --> tests/unit_tests/core/workflow/test_node_runtime.py:579:12
+ERROR Object of class `object` has no attribute `parent_trace_context` [missing-attribute]
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:580:12
 ERROR Object of class `object` has no attribute `trace_session_id` [missing-attribute]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:602:5
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:603:5
 ERROR Argument `object` is not assignable to parameter `variable_pool` with type `VariablePool | None` in function `core.workflow.node_runtime.DifyToolNodeRuntime.get_runtime` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:619:23
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:620:23
 ERROR Object of class `object` has no attribute `trace_session_id` [missing-attribute]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:623:12
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:624:12
 ERROR Argument `object` is not assignable to parameter `variable_pool` with type `VariablePool | None` in function `core.workflow.node_runtime.DifyToolNodeRuntime.get_runtime` [bad-argument-type]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:651:23
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:652:23
 ERROR Object of class `object` has no attribute `tool` [missing-attribute]
-   --> tests/unit_tests/core/workflow/test_node_runtime.py:654:12
+   --> tests/unit_tests/core/workflow/test_node_runtime.py:655:12
 ERROR Argument `TestWorkflowEntry.test_single_step_run_injects_code_limits.StubWorkflow` is not assignable to parameter `workflow` with type `Workflow` in function `core.workflow.workflow_entry.WorkflowEntry.single_step_run` [bad-argument-type]
    --> tests/unit_tests/core/workflow/test_workflow_entry.py:145:22
 ERROR Object of class `_FakeModelNodeMixin` has no attribute `node_data` [missing-attribute]

@WH-2099 WH-2099 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. However, I think using Context for implicit prop drilling here might impose a certain burden on future state management and maintainability.

I would prefer to use an explicit prop-passing (explicit parameter) approach instead.

@ryuta-kobayashi-ug

Copy link
Copy Markdown
Contributor Author

Thanks @WH-2099 — agreed, and I'd like to move toward explicit passing on the host side rather than defend the ContextVar approach. Let me confirm the plan and one design choice with you.

Where app_id has to travel (host side):
AppRunner → ModelInstance.invoke_llm() → graphon LargeLanguageModel.invoke() → _invoke_llm_via_runtime() → PluginModelRuntime.invoke_llm().

The two middle hops live in the external langgenius/graphon package, which is why I originally reached for a ContextVar to keep the change inside the Dify host. But I agree the implicit flow hurts readability and is a latent footgun around streaming/async, so I'm happy to do the explicit, cross-package version.

Proposed approach (my recommendation):
Thread the value explicitly through the call stack down to PluginModelRuntime, but instead of a bare app_id argument I'd pass a small invocation_context object (e.g. { "app_id": ... }). Rationale: it keeps graphon's generic invoke() signature from accruing one Dify-specific provider-metadata field, and it leaves room for conversation_id / workflow_run_id later without another signature change. Concretely this touches:

  • the 6 ModelInstance.invoke_* methods (Dify host),
  • graphon's invoke() + the *ModelRuntime protocols (separate PR in langgenius/graphon),
  • PluginModelRuntime reads it from the context object and forwards it.

Why not constructor injection (the way user_id is handled)?
A PluginModelRuntime is composed only from (tenant_id, user_id) and has no notion of an app, so user_id is part of the runtime identity. app_id is a property of the individual invocation — present for app execution, None for RAG indexing, title generation, suggested questions, etc. So it has to ride with the call, not the runtime.

SDK side (dify-plugin-sdks#313) is the one place I'd keep ContextVar. There _invoke() is an @abstractmethod implemented by every published plugin and __init__ is @final, so adding a parameter would raise TypeError across the ecosystem. get_current_session() keeps it opt-in and backward compatible.

One thing I need your call on: are you OK with a companion PR against langgenius/graphon to extend invoke() / the runtime protocols? If yes, I'll rework #35859 along these lines and open the graphon PR. If you'd rather not touch graphon's signature right now, I can keep the ContextVar bridge tightly scoped and documented as an interim step.

@WH-2099 WH-2099 self-assigned this Jun 15, 2026
@WH-2099

WH-2099 commented Jun 15, 2026

Copy link
Copy Markdown
Member

@ryuta-kobayashi-ug Thanks for the detailed breakdown. I completely agree with your rationale. Threading an context object is indeed the cleanest, most extensible design here, and keeping the SDK side backward-compatible via ContextVar makes perfect sense.

Let's go with your proposed approach. Modifying graphon to achieve the ideal implementation is definitely the right move. Please go ahead and open the companion PR against langgenius/graphon alongside your reworked PR here. I'll follow up and keep an eye on both.

Looking forward to your PRs!

@WH-2099

WH-2099 commented Jun 18, 2026

Copy link
Copy Markdown
Member

@ryuta-kobayashi-ug Hi, I haven't seen any new progress on this task yet. To avoid duplicating our efforts, I wanted to check if you're still working on it.

If you haven't started yet, I'm happy to take over from here and finish the rest.

@ryuta-kobayashi-ug

Copy link
Copy Markdown
Contributor Author

@WH-2099 Thanks for the go-ahead, and sorry for the quiet stretch on my end — yes, I'm still on it and starting today.

Plan for the next push:

  1. Open the companion PR against langgenius/graphon first: introduce a small invocation_context object and thread it through invoke() and the *ModelRuntime protocols (LLM, text-embedding, rerank, tts, speech2text, moderation).
  2. Rework this PR on top of it: drop the ContextVar bridge, pass invocation_context explicitly from the 6 ModelInstance.invoke_* methods down to PluginModelRuntime, and populate app_id from the app runners.
  3. Leave dify-plugin-sdks#313 on the ContextVar approach as discussed, since an explicit _invoke parameter would break existing plugins.

I'll link the graphon PR here as soon as it's up so you can track both together. Thanks for your patience, and for offering to take it on.

@ryuta-kobayashi-ug

Copy link
Copy Markdown
Contributor Author

@WH-2099 Quick update: the graphon companion PR is up — langgenius/graphon#191.

It threads the existing invocation_context (Mapping[str, object] | None) from the public invoke() surface down to model_runtime.invoke_* for all six model types, and adds it to the runtime protocols. It's keyword-only with a None default, so it's fully backward compatible. (invocation_context already existed in graphon but was only wired to the LLM callbacks, never forwarded to the runtime — so this is a small, additive change.) The structured-output entry point is intentionally left out of scope for now.

Next I'll rework this PR on top of it: drop the ContextVar bridge and pass invocation_context={"app_id": ...} explicitly from the app runners down through ModelInstancePluginModelRuntime → the plugin client. I've already validated the seam locally — with graphon#191 installed, app_id flows end-to-end from LargeLanguageModel.invoke(...) to the client, and Dify's existing unit suites stay green.

I'll push the reworked branch here once I've extended coverage to the agent and workflow call sites (not just chat/completion), and will bump the graphon pin once #191 is released. Thanks!

@ryuta-kobayashi-ug

Copy link
Copy Markdown
Contributor Author

@WH-2099 Thanks for merging langgenius/graphon#191 (and for renaming the parameter to request_metadata — agreed, that's clearer).

The Dify-side rework is ready on my end: ContextVar bridge removed, and request_metadata={"app_id": ...} is threaded explicitly from the app runners (and the workflow LLM nodes) through ModelInstancePluginModelRuntime → the plugin client, covering all five app types (chat, completion, agent, advanced-chat/chatflow, workflow). Unit tests are green locally.

One sequencing question before I push, though: request_metadata only landed in graphon v0.6.0, but main here still pins graphon==0.5.3, and v0.6.0 isn't import-compatible with Dify yet — e.g. v0.6.0 dropped graphon.nodes.human_input.enums (HumanInputFormKind / HumanInputFormStatus), which api/models/human_input.py imports and uses in a DB column. So bumping the pin to 0.6.0 breaks Dify at import time, for reasons unrelated to this PR.

What's the plan for moving Dify to graphon 0.6.0? Is a migration already in progress (so I can rebase this PR on top once it lands), or would you prefer I hold until then? Happy to follow whichever path you prefer — I just don't want to bundle an unrelated 0.6.0 migration into this PR.

@WH-2099

WH-2099 commented Jul 6, 2026

Copy link
Copy Markdown
Member

Thanks for the work so far. I will take over the remaining work on this PR and rework it according to the agreed direction. I will keep the change focused and follow up with updates once it is ready for review.

@WH-2099 WH-2099 force-pushed the feat/pass-app-id-to-model-plugins branch from 8459051 to a07788c Compare July 6, 2026 05:37
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Jul 6, 2026
WH-2099
WH-2099 previously approved these changes Jul 6, 2026
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jul 6, 2026
@WH-2099 WH-2099 enabled auto-merge July 6, 2026 06:10
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 52.38% 52.39% +0.01%
Strict coverage 51.89% 51.90% +0.01%
Typed symbols 32,082 32,090 +8
Untyped symbols 29,439 29,440 +1
Modules 2970 2970 0

@WH-2099 WH-2099 force-pushed the feat/pass-app-id-to-model-plugins branch from f3084a7 to fa022d6 Compare July 6, 2026 08:09
@autofix-ci autofix-ci Bot requested a review from crazywoola as a code owner July 6, 2026 08:20
@github-actions github-actions Bot added the web This relates to changes on the web. label Jul 6, 2026
@WH-2099 WH-2099 added this pull request to the merge queue Jul 6, 2026
Merged via the queue into langgenius:main with commit 64d72c6 Jul 6, 2026
37 checks passed
@ryuta-kobayashi-ug

Copy link
Copy Markdown
Contributor Author

Thanks @WH-2099 for taking this over the finish line and for the reworks — great to see it land on main and already ride along in 1.16.0-rc1. Appreciated the collaboration throughout the design discussion. 🙏

@WH-2099

WH-2099 commented Jul 11, 2026

Copy link
Copy Markdown
Member

@ryuta-kobayashi-ug I apologize that the implementation of this feature may have taken a little longer than expected, but I believe the final implementation here is a more robust version for future maintenance and long-term evolution.

Thank you for your patience and contribution.

There's a minor follow-up issue: the new support has been merged into the dify-plugin-sdk mainline (langgenius/dify-plugin-sdks#313).
We now need to plan the release schedule for dify-plugin-sdks.
Do you need to release it as soon as possible to ensure that the relevant changes in dify-official-plugins are reimplemented and implemented?

@ryuta-kobayashi-ug

Copy link
Copy Markdown
Contributor Author

@WH-2099 Thank you again, and no worries at all about the timeline — I fully agree the final design is more robust and well worth the extra iterations.

On the release question: if it's feasible on your side, I'd gently advocate for releasing dify-plugin-sdks relatively soon. The host-side change now in Dify 1.16.0-rc1 forwards request_metadata (including app_id) all the way down to the plugin, but until a release containing #313 is available, the SDK still discards the session before it reaches the model implementation — so the end-to-end path stays inert and the host-side work can't actually surface to plugins yet. An earlier release would unblock dify-official-plugins to adopt get_current_session() and make provider-side cost attribution work end-to-end.

That said, I don't want to rush anything — if it's cleaner to fold this into your normal release cadence, that's completely fine by me. And I'm glad to help with the dify-official-plugins side once the SDK is out. Thanks again for taking this all the way to the finish line!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer size:M This PR changes 30-99 lines, ignoring generated files. web This relates to changes on the web.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pass app_id to model plugins for provider-side cost attribution

2 participants