> ## 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

> Call models or transports outside the built-in Pi runtime.

A custom executor replaces provider execution for one or more model declarations. Use one when a model isn't in Pi's catalog, needs a private transport, or requires provider-specific behavior outside the shared HTTP contract.

Policies stay independent — they choose a model, Router Core dispatches to whatever executor that model declares.

## Return A Completion

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

const acmeExecutor: RouterExecutor = {
  async execute({ request, model, decision, signal }) {
    const result = await acme.generate({
      model: model.id,
      input: 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-model", executor: "acme" }],
  policy: ({ candidates }) => ({ model: candidates[0]!.id }),
  executors: { acme: acmeExecutor },
});
```

Completion content can include text, tool calls, reasoning, and hosted `web_search` calls. Finish reason must be `stop`, `length`, or `tool_calls`.

## Return A Stream

<Accordion title="Stream event types">
  The normalized stream contract uses seven event types:

  ```ts theme={null}
  type RouterStreamEvent =
    | { type: "text_delta"; index: number; delta: string }
    | { type: "tool_call_start"; index: number; id: string; name: string }
    | { type: "tool_call_delta"; index: number; delta: string }
    | { type: "tool_call_end"; index: number }
    | { type: "reasoning_delta"; index: number; delta: string }
    | {
        type: "reasoning_end";
        index: number;
        redacted?: boolean;
        source?: RouterProviderIdentity;
        itemId?: string;
        continuation?: ProviderContinuationState;
      }
    | { type: "finish"; finishReason: "stop" | "length" | "tool_calls"; usage?: RouterUsage };
  ```

  `reasoning_delta` streams readable thinking text; `reasoning_end` closes the block and may carry the provider identity and stable item ID used to sign plain reasoning, or an encrypted `continuation`. Redacted thinking is a bare `reasoning_end` with `redacted: true` and no prior deltas.

  Router Core validates ordering: tool calls must start before their deltas end, an index can't mix text, reasoning, and open tool calls, open tools and open reasoning blocks must close before `finish`, and nothing follows `finish`.
</Accordion>

```ts theme={null}
const streamingExecutor: RouterExecutor = {
  execute() {
    return {
      type: "stream",
      events: {
        async *[Symbol.asyncIterator]() {
          yield { type: "text_delta", index: 0, delta: "Hello" } as const;
          yield { type: "text_delta", index: 0, delta: " world" } as const;
          yield { type: "finish", finishReason: "stop", usage: { inputTokens: 10, outputTokens: 2 } } as const;
        },
      },
    };
  },
};
```

## Declare Capabilities

Custom models need explicit capability metadata so Router Core knows which requests they can handle:

```ts theme={null}
import type { RouterModel } from "@dari/router-core";

const model = {
  id: "acme/private-model",
  executor: "acme",
  provider: "acme",
  api: "private-rpc",
  reasoningEfforts: ["off", "high"],
  defaultReasoningEffort: "off",
  capabilities: { imageInput: true, toolUse: true, structuredOutput: true, streaming: true },
} satisfies RouterModel;
```

The `satisfies RouterModel` ascription matters for standalone declarations: without it, `reasoningEfforts` infers as `string[]` and the object no longer type-checks when passed to `createRouter`. Objects written inline inside `models: [...]` are contextually typed and don't need it.

When omitted: provider defaults to the prefix before `/`, API defaults to the executor name, reasoning efforts default to `["off"]`, all capabilities default to `false`.

## Cancellation And Cleanup

The executor receives the same `AbortSignal` as the policy. Request cancellation or response-body cancellation abort it. Router Core calls `return()` on the stream iterator when streaming ends early. Release transport resources in a `finally` block.

## Retry And Fallback

The executor owns same-model retries — Router Core doesn't classify provider errors or decide what's retryable.

Cross-model fallback belongs to `createRouter`: with `fallback: { enabled: true }`, when the selected model's executor call fails, the router retries once on the first other eligible candidate before returning an error. `requiresDifferentProvider: true` restricts the fallback to another provider. The response reports the model that actually served — in `dari_routing` and the `X-Router-Selected-Model` header — and if the fallback model doesn't support the decision's reasoning effort, its own default effort is used. A lease pinning the failed primary is dropped so the next turn selects fresh.

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

For richer behavior — error classification, retry budgets, billing-aware fallback chains — host the phased API yourself or use the [managed platform](/router/overview).
