Repository navigation
[feat] Expose a guarded host model invocation capability to extensions #5679
Description
Background
DeerFlow extensions can currently:
- observe system model calls through
SystemModelCallObserver; - expose model tools through
PluginContribution; - access explicitly granted host capabilities through
ExtensionRuntimeDeps.
However, an extension service cannot actively invoke a model already configured in DeerFlow.
This creates a general capability gap for extensions that need model-assisted processing outside an agent run, such as:
- classification and content transformation;
- structured information extraction;
- summarization;
- validation and quality evaluation;
- metadata generation;
- background processing triggered by extension-owned workflows.
Currently, an extension must configure its own provider credentials and duplicate parts of DeerFlow's model resolution, provider adaptation, authorization, timeout, and observability behavior.
Proposed capability
Expose an optional, host-provided model invocation capability through ExtensionRuntimeDeps.
Conceptually:
class ExtensionService:
async def start(self, deps: ExtensionRuntimeDeps) -> None:
self.model_invoker = deps.model_invokerThe public extension contract could use narrow, provider-neutral request and response types:
@dataclass(frozen=True)
class ModelInvocationRequest:
messages: Sequence[ModelMessage]
model_role: str | None = None
purpose: str | None = None
response_schema: Mapping[str, object] | None = None
timeout_seconds: float | None = None
@dataclass(frozen=True)
class ModelInvocationResult:
content: str
structured_output: Mapping[str, object] | None = None
resolved_model: str | None = None
usage: ModelUsage | None = NoneThe exact types are open for discussion.
The important boundary is that extensions should not receive provider clients, API keys, the complete AppConfig, or internal model framework objects.
Model selection
The request should identify an optional logical model_role, rather than a deployment-specific model name.
For example, a deployment may define roles such as default, fast, or reasoning. The host maps an authorized role to a configured model. This allows operators to replace or reroute models without changing extension code.
If no role is supplied, the host may use the deployment-defined default extension or system model.
The extension should not supply arbitrary provider credentials or construct the host model directly.
Authorization
Model invocation should be an explicit, source-bound host capability, following the guarded capability pattern used by other extension runtime dependencies.
For example:
plugins:
- name: example-extension
use: example_package:install
host_access:
model_invocation:
roles: [default]
max_concurrency: 2Possible host-enforced restrictions include:
- allowed logical model roles;
- maximum concurrency;
- timeout and output limits;
- optional purpose identifiers;
- revocation when the plugin grant or source changes.
The first implementation does not need to provide every quota mechanism, but the contract should allow the host to enforce policy without exposing credentials.
Host responsibilities
The host invocation implementation should retain responsibility for:
- resolving logical roles to configured models;
- provider construction and credentials;
- model authorization;
- timeout and cancellation;
- tracing and usage metadata;
- normalized errors;
- optional structured-output handling.
Initial scope
A minimal first version only needs:
- non-streaming asynchronous invocation;
- text messages;
- optional structured output;
- timeout and cancellation;
- explicit unavailable, unauthorized, and invocation-failed errors.
The following can remain out of scope initially:
- Agent or LangGraph execution;
- tool calling;
- token streaming;
- conversation persistence;
- direct access to provider SDKs;
- access to raw DeerFlow configuration or credentials.
Relationship to existing extension APIs
This differs from SystemModelCallObserver, which passively observes model calls initiated by the host.
It also differs from plugin ModelTool contributions, where an extension exposes a tool for a DeerFlow agent to invoke.
The proposed capability allows an authorized extension service to initiate a bounded model call while DeerFlow retains ownership of model configuration, credentials, routing, and policy enforcement.
Example
An extension receives extension-owned input and requests a structured transformation:
result = await model_invoker.invoke(
ModelInvocationRequest(
model_role="default",
messages=[
ModelMessage(
role="user",
content="Extract the requested fields from this content.",
)
],
response_schema={
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {"type": "string"},
}
},
"required": ["items"],
},
)
)The extension owns its workflow and persistence. DeerFlow owns model selection, credentials, authorization, and invocation behavior.
Compatibility
The capability should be optional:
- existing extensions remain unchanged;
deps.model_invokeris absent orNonewhen unsupported or not granted;- deployments without extensions incur no model invocation overhead.
Open questions
- Should logical model roles be built in, deployment-defined, or both?
- Should structured output be included in the initial contract?
- Which resource limits should be part of the public contract, and which should remain host implementation details?
Activity
Totoro-qaq commentedon Sep 22, 2026
The text-list/CSV classification prototype in #5653, which compares Jev and LLM backends, could provide a concrete consumer example for this capability. Its LLM backend would benefit from reusing a host-configured model without maintaining a separate credential/client path.
For v1, I’d favor optional structured output with a clear success contract: when
response_schemais supplied, return schema-validated data or an explicit error. File handling, batch grouping, and application checks such as matching returned row IDs to inputs can stay in the extension.Deployment-defined model roles would be sufficient for this use case. Would a small classification example be useful for validating the API and its failure behavior?
zhfeng commentedon Sep 22, 2026
Thanks — #5653 looks like a useful concrete consumer for validating this capability.
I agree with the proposed v1 structured-output contract: when
response_schemais provided, a successful invocation should return schema-validated data. Provider output that cannot be parsed or validated should produce an explicit invocation/validation error rather than a partially populated success result.Deployment-defined roles also seem sufficient for the initial contract. This keeps extensions portable while allowing operators to map roles to their configured models and providers.
A small classification example would be useful, especially if it covers:
The example can keep batching, row-ID reconciliation, file handling, and application-specific validation outside the host capability, as you suggested.
Totoro-qaq commentedon Sep 22, 2026
Thanks, the example scope makes sense. Before adapting it, could we align the capability boundary with #5624’s planned shared TypeSafe client?
The proposed model_invoker takes messages and an optional response schema, whereas Jev evaluates state and typed questions and returns answers/probabilities. Is model_invoker intended for chat-style calls, with typed decision evaluation exposed through a separate capability if needed?
My preference would be to share the host authorization and lifecycle conventions, reuse the TypeSafe client across its consumers, and keep workflow-specific policies in the adapters. Documenting that boundary now would help the classification example and future integrations follow a consistent design.
Totoro-qaq commentedon Sep 22, 2026
Answering my own boundary question, since the two PRs opened since then settle it.
TypeSafeGuardrailProviderand its error type; there is no shared TypeSafe client for other consumers to import.httpxclient and reads the key from a deployment-named environment variable.So each extension owns its Jev calls, and
model_invokerstays a chat-style capability for host-configured models. No separate typed-decision capability is needed for the classification case.On that basis I will adapt the #5653 prototype into a standalone example package under
examples/, in the same shape as #5731: the Jev backend uses its own client and an environment-only key, while the LLM backend is wheremodel_invokercomes in. The example will cover the five cases listed above: schema-validated success, malformed provider output, schema validation failure, unavailable or unauthorized model role, and timeout and cancellation. Batching, row-ID reconciliation and file handling stay inside the extension.Until
model_invokerexists, the LLM backend will use its own environment-named key in the same way, and switch tomodel_invokeronce the contract is available. I will link the PR here.