Skip to content

[feat] Expose a guarded host model invocation capability to extensions #5679

Closed

Description

@zhfeng

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_invoker

The 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 = None

The 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: 2

Possible 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_invoker is absent or None when unsupported or not granted;
  • deployments without extensions incur no model invocation overhead.

Open questions

  1. Should logical model roles be built in, deployment-defined, or both?
  2. Should structured output be included in the initial contract?
  3. Which resource limits should be part of the public contract, and which should remain host implementation details?

Activity

  1. Totoro-qaq commented on Sep 22, 2026

    @Totoro-qaq
    Contributor

    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_schema is 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?

  2. zhfeng commented on Sep 22, 2026

    @zhfeng
    CollaboratorAuthor

    Thanks — #5653 looks like a useful concrete consumer for validating this capability.

    I agree with the proposed v1 structured-output contract: when response_schema is 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:

    • successful schema-validated output;
    • malformed provider output;
    • schema validation failure;
    • unavailable or unauthorized model role;
    • timeout and cancellation.

    The example can keep batching, row-ID reconciliation, file handling, and application-specific validation outside the host capability, as you suggested.

  3. Totoro-qaq commented on Sep 22, 2026

    @Totoro-qaq
    Contributor

    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.

  4. Totoro-qaq commented on Sep 22, 2026

    @Totoro-qaq
    Contributor

    Answering my own boundary question, since the two PRs opened since then settle it.

    So each extension owns its Jev calls, and model_invoker stays 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 where model_invoker comes 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_invoker exists, the LLM backend will use its own environment-named key in the same way, and switch to model_invoker once the contract is available. I will link the PR here.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageAwaiting maintainer triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions