> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dari.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Executors

> Connect models to your own SDK, service, or transport.

An executor runs a selected candidate. Use one when your provider is not in Pi's catalog, your application has a private gateway, or you need a specialized SDK.

## Completion executor

```ts theme={null}
import { createRouter, type RouterExecutor } from "@mupt-ai/dari-router";

const acme: RouterExecutor = {
  async execute({ request, model, decision, signal }) {
    const result = await acmeSdk.generate({
      model: model.id,
      items: request.items,
      reasoningEffort: decision.reasoningEffort,
      signal,
    });
    return {
      type: "complete",
      output: {
        content: [{ type: "text", text: result.text }],
        finishReason: "stop",
        usage: { inputTokens: result.inputTokens, outputTokens: result.outputTokens },
      },
    };
  },
};

const router = createRouter({
  models: [{ id: "acme/private", executor: "acme" }],
  policy: ({ candidates }) => ({ model: candidates[0]!.id }),
  executors: { acme },
});
```

The executor receives the normalized `RouterRequest`, the selected candidate, the validated decision, and an abort signal. A completion can contain text, tool calls, reasoning, and hosted tool calls. Its finish reason is `stop`, `length`, or `tool_calls`.

## Streaming executor

Return an async iterable when `request.stream` is true:

```ts theme={null}
const executor: RouterExecutor = {
  execute({ request }) {
    if (!request.stream) throw new Error("This example only streams");
    return {
      type: "stream",
      events: {
        async *[Symbol.asyncIterator]() {
          yield { type: "text_delta", index: 0, delta: "Hello" } as const;
          yield { type: "finish", finishReason: "stop" } as const;
        },
      },
    };
  },
};
```

The event sequence must be valid: start tool calls before their deltas, close tools and reasoning before `finish`, and emit nothing after `finish`. The full event union is exported as `RouterStreamEvent`.

## Model metadata

Custom models should declare the capabilities they genuinely support:

```ts theme={null}
const model = {
  id: "acme/private",
  executor: "acme",
  provider: "acme",
  api: "private-rpc",
  reasoningEfforts: ["off"],
  capabilities: { streaming: true, toolUse: false, imageInput: false, structuredOutput: false },
} as const;
```

Capability metadata controls eligibility. It does not add features to your transport; your executor must implement the contract it advertises.

## Cancellation and cleanup

Propagate `signal` to the SDK. Request cancellation and response-body cancellation abort it. For streams, release resources in `finally`; Router Core closes an async iterator when a stream ends early.

## Fallback

Same-model retries belong to your executor. To retry once on another eligible candidate:

```ts theme={null}
const router = createRouter({
  models,
  policy,
  executors: { acme },
  fallback: { enabled: true, requiresDifferentProvider: true },
});
```

`requiresDifferentProvider` restricts the retry to another provider. The response, hooks, and routing metadata report the model that actually served. A failed primary's lease is removed. For retry budgets, error classification, or multi-step recovery, own that orchestration outside `createRouter`.
