# API Keys Source: https://docs.dari.dev/api-keys Create Routing and Management keys. | Type | Use | | ---------- | -------------------------------------------------------------------------------------------------- | | Routing | Send model requests through a router. | | Management | Manage routers, read model activity, manage credentials, evals, API keys, and organization access. | ## Create A Routing Key ```bash theme={null} dari api-keys create --name router-client --type routing export DARI_ROUTING_API_KEY="dari_..." ``` ## Create A Management Key ```bash theme={null} dari auth login dari api-keys create --name automation export DARI_API_KEY="dari_..." ``` You can also create either key from **API Keys** in the [Dari Dashboard](https://app.dari.dev). Keys are shown once. Store them in a secret manager and use a Routing key from the same organization as the router. Provider credentials are separate; configure them on the router's [Keys page](/router/configure-provider-keys). # Routing Activity API Source: https://docs.dari.dev/api-reference/managed/activity GET https://api.dari.dev/v1/organizations/current/routing/activity/overview Read routing telemetry for one organization. `Bearer` plus a Management API key. RFC 3339 range start, inclusive. RFC 3339 range end, exclusive. Limit to one router. API key ID filter. Repeatable. Attributed user ID filter. Repeatable. Model ID filter. Repeatable. Provider filter. `completed`, `provider_error`, `selector_error`, or `aborted`. `user`, `managed`, or `subscription`. `openai_chat`, `openai_responses`, `anthropic_messages`, comma-separated, `all`, or `none`. `60`, `300`, `900`, `1800`, `86400`, `604800`, or `2592000`. ```bash cURL theme={null} curl "https://api.dari.dev/v1/organizations/current/routing/activity/overview?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z&bucket_seconds=86400" \ -H "Authorization: Bearer $DARI_API_KEY" ``` The overview response contains `summary`, `series`, `model_mix`, `api_keys`, and `key_sources`. Activity ranges cannot exceed 366 days or 1000 time buckets. ## Other Activity Endpoints Discover user, API-key, router, model, and provider IDs. Per-model cost, latency, outcomes, and transitions. Attributed people and API keys, including comparisons over time. Conversation lists and routed steps. Observed capability usage, inventories, and details. ## CLI Equivalence The CLI maps directly to these HTTP endpoints: | CLI command | Endpoint | | ------------------------------------------ | ------------------------------------------------- | | `dari activity filter-options` | `GET .../routing/activity/filter-options` | | `dari activity overview` | `GET .../routing/activity/overview` | | `dari activity models` | `GET .../routing/activity/models` | | `dari activity people` | `GET .../routing/activity/people-keys` | | `dari activity people series` | `GET .../routing/activity/people-series` | | `dari activity conversations` | `GET .../routing/activity/conversations` | | `dari activity conversations get ` | `GET .../routing/activity/conversations/{ref}` | | `dari activity tools` / `skills` | `GET .../routing/activity/tools-skills` | | `dari activity tools list` / `skills list` | `GET .../routing/activity/tools-skills/inventory` | | `dari activity tools get` / `skills get` | `GET .../routing/activity/tools-skills/detail` | ```bash theme={null} dari activity overview --from 2026-07-01T00:00:00Z --to 2026-07-08T00:00:00Z ``` # Conversations Source: https://docs.dari.dev/api-reference/managed/activity/conversations GET https://api.dari.dev/v1/organizations/current/routing/activity/conversations List routed conversations and inspect their model steps. `Bearer` plus a Management API key. RFC 3339 range start, inclusive. RFC 3339 range end, exclusive. Limit to one router. API key ID filter. Repeatable. Attributed user ID filter. Repeatable. Model ID filter. Repeatable. Provider filter. `completed`, `provider_error`, `selector_error`, or `aborted`. `user`, `managed`, or `subscription`. `openai_chat`, `openai_responses`, `anthropic_messages`, comma-separated, `all`, or `none`. Search conversation identifiers and titles. `last_active`, `messages`, `model_steps`, `model_switches`, `spend`, or `tokens`. `asc` or `desc`. Maximum conversations. Conversations to skip. ```bash cURL theme={null} curl "https://api.dari.dev/v1/organizations/current/routing/activity/conversations?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z&sort_by=model_switches" \ -H "Authorization: Bearer $DARI_API_KEY" ``` The response contains `conversations`, `conversation_count`, switch totals, and pagination fields. ## Conversation Detail ```http theme={null} GET /v1/organizations/{org}/routing/activity/conversations/{conversation_ref} ``` Conversation identity from the list response. Maximum model steps. Steps to skip from the newest end. ```bash theme={null} dari activity conversations get ``` # Activity Filter Options Source: https://docs.dari.dev/api-reference/managed/activity/filter-options GET https://api.dari.dev/v1/organizations/current/routing/activity/filter-options List users, API keys, routers, models, and providers available to activity filters. Use this endpoint to discover IDs before calling activity endpoints with `user_id`, `api_key_id`, or `router_id`. `Bearer` plus a Management API key. RFC 3339 range start, inclusive. RFC 3339 range end, exclusive. `openai_chat`, `openai_responses`, `anthropic_messages`, comma-separated, `all`, or `none`. ```bash cURL theme={null} curl "https://api.dari.dev/v1/organizations/current/routing/activity/filter-options?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z" \ -H "Authorization: Bearer $DARI_API_KEY" ``` The response contains `routers`, `api_keys`, `users`, `models`, and `providers`. User entries include `id`, `name`, and `email`. ```bash theme={null} dari activity filter-options \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z | jq '.users[] | {id, name, email}' ``` # Model Activity Source: https://docs.dari.dev/api-reference/managed/activity/models GET https://api.dari.dev/v1/organizations/current/routing/activity/models Per-model usage, cost, latency, outcomes, pricing coverage, and route transitions. `Bearer` plus a Management API key. RFC 3339 range start, inclusive. RFC 3339 range end, exclusive. Limit to one router. API key ID filter. Repeatable. Attributed user ID filter. Repeatable. Model ID filter. Repeatable. Provider filter. `completed`, `provider_error`, `selector_error`, or `aborted`. `user`, `managed`, or `subscription`. `openai_chat`, `openai_responses`, `anthropic_messages`, comma-separated, `all`, or `none`. ```bash cURL theme={null} curl "https://api.dari.dev/v1/organizations/current/routing/activity/models?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z&status=provider_error" \ -H "Authorization: Bearer $DARI_API_KEY" ``` The response contains `summary`, `models`, and `transitions`. Model rows include steps, spend, pricing coverage, p95 provider latency, and non-completion rate. Unknown pricing is not treated as free usage. ```bash theme={null} dari activity models --from 2026-07-01T00:00:00Z --to 2026-07-08T00:00:00Z --status provider_error ``` # Activity Overview Source: https://docs.dari.dev/api-reference/managed/activity/overview GET https://api.dari.dev/v1/organizations/current/routing/activity/overview Usage, spend, savings, tokens, cache, model mix, key sources, and API-key activity. `Bearer` plus a Management API key. RFC 3339 range start, inclusive. RFC 3339 range end, exclusive. Limit to one router. API key ID filter. Repeatable. Attributed user ID filter. Repeatable. Model ID filter. Repeatable. Provider filter. `completed`, `provider_error`, `selector_error`, or `aborted`. `user`, `managed`, or `subscription`. `openai_chat`, `openai_responses`, `anthropic_messages`, comma-separated, `all`, or `none`. `60`, `300`, `900`, `1800`, `86400`, `604800`, or `2592000`. ```bash cURL theme={null} curl "https://api.dari.dev/v1/organizations/current/routing/activity/overview?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z&bucket_seconds=86400" \ -H "Authorization: Bearer $DARI_API_KEY" ``` The response contains `summary`, `series`, `model_mix`, `api_keys`, and `key_sources`. Activity ranges cannot exceed 366 days or 1000 time buckets. ```bash theme={null} dari activity overview --from 2026-07-01T00:00:00Z --to 2026-07-08T00:00:00Z ``` # People & Keys Source: https://docs.dari.dev/api-reference/managed/activity/people GET https://api.dari.dev/v1/organizations/current/routing/activity/people-keys List routed activity attributed to people and API keys. `Bearer` plus a Management API key. RFC 3339 range start, inclusive. RFC 3339 range end, exclusive. Limit to one router. API key ID filter. Repeatable. Attributed user ID filter. Repeatable. Model ID filter. Repeatable. Provider filter. `completed`, `provider_error`, `selector_error`, or `aborted`. `user`, `managed`, or `subscription`. `openai_chat`, `openai_responses`, `anthropic_messages`, comma-separated, `all`, or `none`. Search names, emails, key labels, and prefixes. `all`, `people`, or `keys`. `identity`, `keys`, `conversations`, `messages`, `tokens`, `spend`, or `last_active`. `asc` or `desc`. Maximum identities. Identities to skip. ```bash cURL theme={null} curl "https://api.dari.dev/v1/organizations/current/routing/activity/people-keys?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z&identity_scope=keys&search=prod" \ -H "Authorization: Bearer $DARI_API_KEY" ``` The response contains `identities`, `identity_count`, `limit`, `offset`, and `next_offset`. ## Compare People Over Time ```http theme={null} GET /v1/organizations/{org}/routing/activity/people-series ``` Use `60`, `300`, `900`, `1800`, `86400`, `604800`, or `2592000`. Person to compare. Repeatable. ```bash theme={null} dari activity people series --from 2026-07-01T00:00:00Z --to 2026-07-08T00:00:00Z --comparison-user-id usr_123 ``` # Tools & Skills Source: https://docs.dari.dev/api-reference/managed/activity/tools-and-skills GET https://api.dari.dev/v1/organizations/current/routing/activity/tools-skills Read observed tool and skill usage, inventories, and capability details. `Bearer` plus a Management API key. RFC 3339 range start, inclusive. RFC 3339 range end, exclusive. Limit to one router. API key ID filter. Repeatable. Attributed user ID filter. Repeatable. Model ID filter. Repeatable. Provider filter. `completed`, `provider_error`, `selector_error`, or `aborted`. `user`, `managed`, or `subscription`. `openai_chat`, `openai_responses`, `anthropic_messages`, comma-separated, `all`, or `none`. `tools` or `skills`. Use `60`, `300`, `900`, `1800`, `86400`, `604800`, or `2592000`. Capability to include in the time series. Repeatable. ```bash cURL theme={null} curl "https://api.dari.dev/v1/organizations/current/routing/activity/tools-skills?mode=tools&from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z&bucket_seconds=86400&series_id=web_search" \ -H "Authorization: Bearer $DARI_API_KEY" ``` The response contains `total_uses`, `total_items`, `items_truncated`, `items`, `series`, and `selected_series_ids`. Without explicit `series_id` values, the response includes at least five ranked series and expands up to ten until they represent 90% of the selected metric. ## Inventory ```http theme={null} GET /v1/organizations/{org}/routing/activity/tools-skills/inventory ``` Search capability names and IDs. `name`, `uses`, or `latest`. `asc` or `desc`. Maximum capabilities. Capabilities to skip. ```bash theme={null} dari activity tools list --from 2026-07-01T00:00:00Z --to 2026-07-08T00:00:00Z --search search ``` ## Capability Detail ```http theme={null} GET /v1/organizations/{org}/routing/activity/tools-skills/detail ``` Exact capability identity from inventory. ```bash theme={null} dari activity tools get web_search --from 2026-07-01T00:00:00Z --to 2026-07-08T00:00:00Z ``` The detail response contains `item`, `series`, `identities`, `versions`, and `sources`. # Management API Overview Source: https://docs.dari.dev/api-reference/managed/overview Manage routers, keys, credentials, evals, organizations, and activity over HTTP. The management API at `https://api.dari.dev` backs the `dari` CLI. Every CLI command is a thin wrapper over one HTTP endpoint, so pipelines can use either interface. ## Authentication Send a Management key as a bearer token: ```bash theme={null} export DARI_API_KEY="dari_..." ``` ```bash theme={null} curl -sS https://api.dari.dev/v1/organizations/current/routing/activity/overview?from=...&to=... \ -H "Authorization: Bearer $DARI_API_KEY" ``` Routing keys cannot read management endpoints. ## Organization Scoping Management paths are scoped to one organization: * `current` — the organization for the credential. * An explicit `org_...` ID — requires a browser login session with access to that organization. Management keys cannot switch organizations. ## Endpoint Areas Every path below is relative to `https://api.dari.dev`: | Area | Base path | CLI | | --------------------------------------------------- | ------------------------------------------------- | ------------------ | | Routers | `/v1/organizations/{org}/routers` | `dari router` | | API keys | `/v1/organizations/{org}/api-keys` | `dari api-keys` | | Credentials | `/v1/organizations/{org}/credentials` | `dari credentials` | | Evals | `/v1/organizations/{org}/evals` | `dari eval` | | Members & invitations | `/v1/organizations/{org}/members`, `/invitations` | `dari org` | | [Routing activity](/api-reference/managed/activity) | `/v1/organizations/{org}/routing/activity` | `dari activity` | [Routing activity](/api-reference/managed/activity) is the read-only telemetry surface: overview, models, people, conversations, tools, and skills. # Chat Completions API Source: https://docs.dari.dev/api-reference/router/chat-completions POST https://routing.dari.dev/v1/chat/completions Send a routed OpenAI-compatible chat completion request `Bearer` plus a Routing API key. Must be `dari/routing`. OpenAI-style messages. Stream with Server-Sent Events. OpenAI-style tool definitions. Require the selected model to use exactly `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. Models that do not support that level are ineligible. ```bash cURL theme={null} curl https://routing.dari.dev/v1/chat/completions \ -H "Authorization: Bearer $DARI_ROUTING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "dari/routing", "messages": [{"role": "user", "content": "Hello from Dari"}] }' ``` The response follows the OpenAI Chat Completions shape. `dari_routing` adds `selected_model`, `reasoning_effort`, `reason`, and `conversation_id`. The `/v1` endpoint uses your organization's default router. To address a specific router instead, send the same request to `https://routing.dari.dev/{router_id}/chat/completions`. # Anthropic Messages API Source: https://docs.dari.dev/api-reference/router/messages POST https://routing.dari.dev/v1/messages Send a routed Anthropic Messages request `Bearer` plus a Routing API key. Must be `dari/routing`. Maximum output tokens. Anthropic Messages input objects. System instructions. Stream with Server-Sent Events. Anthropic tool definitions. Enable Anthropic extended thinking when the selected model supports it. ```bash cURL theme={null} curl https://routing.dari.dev/v1/messages \ -H "Authorization: Bearer $DARI_ROUTING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "dari/routing", "max_tokens": 256, "messages": [{"role": "user", "content": "Hello from Dari"}] }' ``` The response follows the Anthropic Messages shape. The selected model and reasoning effort are included in the `dari_routing` metadata returned with the message. The `/v1` endpoint uses your organization's default router; use `https://routing.dari.dev/{router_id}/messages` for a specific router. # Router API Overview Source: https://docs.dari.dev/api-reference/router/overview Send routed model traffic over HTTP. Send requests to `https://routing.dari.dev/v1` with a Routing API key and `model: "dari/routing"`. The `/v1` path uses your organization's default router, which starts with Dari's managed model set and requires no setup. ```bash theme={null} curl https://routing.dari.dev/v1/chat/completions \ -H "Authorization: Bearer $DARI_ROUTING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "dari/routing", "messages": [{"role": "user", "content": "Hello from Dari"}] }' ``` Address a specific router by its `rtr_...` ID instead of `/v1`. Both forms accept the same request bodies, and endpoint paths vary by protocol: | Protocol | Default Router | Specific Router | | ------------------ | ---------------------- | ------------------------------- | | Chat Completions | `/v1/chat/completions` | `/{router_id}/chat/completions` | | Responses | `/v1/responses` | `/{router_id}/v1/responses` | | Anthropic Messages | `/v1/messages` | `/{router_id}/v1/messages` | Claude Code, Codex, and Pi take base URLs and append their protocol-specific paths, so use the base URL shown in each coding-agent guide. OpenAI-compatible Chat Completions. OpenAI Responses over HTTP. # Responses API Source: https://docs.dari.dev/api-reference/router/responses POST https://routing.dari.dev/v1/responses Send a routed OpenAI Responses request `Bearer` plus a Routing API key. Must be `dari/routing`. Text or Responses input items. System or developer instructions. Stream Responses events. Function, custom, or namespace tools. Require the selected model to use exactly `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. Models that do not support that level are ineligible. Responses uses `none` where Chat Completions uses `off`. Maximum output tokens. ```bash cURL theme={null} curl https://routing.dari.dev/v1/responses \ -H "Authorization: Bearer $DARI_ROUTING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "dari/routing", "input": "Hello from Dari" }' ``` The response follows the OpenAI Responses shape. `dari_routing` adds `selected_model`, `reasoning_effort`, `reason`, and `conversation_id`. The `/v1` endpoint uses your organization's default router. To address a specific router instead, send the same request to `https://routing.dari.dev/{router_id}/v1/responses`. This endpoint uses HTTP, not WebSockets. `previous_response_id` is unsupported; resend the full input history. # Authentication Source: https://docs.dari.dev/authentication Choose the credential for each Dari workflow. | Credential | Use | | ------------------- | --------------------------------------------------------- | | Dashboard login | Interactive account, organization, and billing workflows. | | Management API key | Headless CLI and management operations. | | Routing API key | Requests sent to a router endpoint. | | Provider credential | Upstream model calls after Dari selects a model. | ## Dashboard And CLI Login ```bash theme={null} dari auth login dari auth status ``` ## Management Key Export a Management key as `DARI_API_KEY`. Supported CLI commands use it instead of cached browser login. ```bash theme={null} export DARI_API_KEY="dari_..." ``` ## Routing Key Use a separate Routing key from the same organization as the router: ```bash theme={null} export DARI_ROUTING_API_KEY="dari_..." ``` See [API Keys](/api-keys) to create keys and [Configure Provider Keys](/router/configure-provider-keys) for upstream credentials. # Router Evals Source: https://docs.dari.dev/evals/overview Add benchmark scores as evidence for model selection. Router evals are model scorecards. Create one from **Evals** in the dashboard or upload a CSV with the Dari CLI: ```csv theme={null} model_id,score,thinking_level,notes openai/gpt-5.6-sol,87,high,Strong public run openai/gpt-5.6-sol,82,off,Non-reasoning run anthropic/claude-sonnet-4-6,81,,Generic score ``` `model_id` and `score` must be the first two columns. Higher scores are better. `thinking_level` and `notes` are optional. The CLI also accepts an optional `metadata_json` column containing a JSON object. Model IDs must exactly match the provider-prefixed IDs enabled on the router, and each model/level pair must be unique. ```bash theme={null} dari eval create \ --name "SWE-bench Verified" \ --description "Public benchmark scores." \ --min-score 0 \ --max-score 100 \ --file scores.csv ``` Use `--file -` to read CSV data from standard input. The command validates the CSV before creating the scorecard and prints the created eval as JSON. Import the eval from the router create or edit page. Dari prefers a score matching both model and reasoning level, then falls back to that model's row with a blank level. Scores inform selection; they do not define a fixed formula. # Auto Router Source: https://docs.dari.dev/framework/auto-router Use Dari's hosted selection policy with your own executors. `createAutoRouter()` is a hosted routing policy. It sends the eligible candidate list and normalized conversation to Dari, receives a decision, and returns it to your local `createRouter`. Your executors still make provider calls and your provider credentials stay in your application. You need a Dari API key with billing enabled. Auto Router selection is billed per selection call. For fully hosted routing and execution, use the [Managed Router](/router/overview). ## Setup ```ts theme={null} import { createAutoRouter, createPiRuntime, createRouter } from "@mupt-ai/dari-router"; const pi = await createPiRuntime({ apiKey: ({ provider }) => { const key = process.env[`${provider.toUpperCase()}_API_KEY`]; if (!key) throw new Error(`Missing ${provider.toUpperCase()}_API_KEY`); return key; }, }); const router = createRouter({ models: [ pi.model("openai/gpt-5.6-sol"), pi.model("anthropic/claude-sonnet-5"), ], policy: createAutoRouter({ apiKey: process.env.DARI_API_KEY! }), executor: pi, }); ``` Auto Router supplies a default selector endpoint and selector model. You can override the endpoint for testing. `createAutoRouter` does not accept a custom selector, selector runtime, selector model, or custom-rule strategy; those belong to `createDariRoutingPolicy` under `/policy-engine`. ## What leaves your process The selection request contains the conversation and eligible candidates. It does not contain provider credentials, request metadata, user identity, or encrypted reasoning continuations. Images are represented by an omission placeholder. Readable message content, tool arguments/results, and readable reasoning may be sent to Dari for selection. Review this data flow before enabling it for sensitive traffic. ## Selection leases A policy may return `leaseTurnsRemaining`. When a request has a `cacheKey` (OpenAI `prompt_cache_key`), `createRouter` stores that lease and serves the same eligible model on subsequent turns without calling the policy. The default store is in memory with a 30-minute TTL. Use a persistent [`leaseStore`](/framework/operations#leases) across replicas. `router.evaluatePolicy()` never reads or writes leases. `router.select()` and `router.fetch()` do. ## Configuration ```ts theme={null} createAutoRouter({ apiKey: process.env.DARI_API_KEY!, endpoint: "https://routing.dari.dev/v1/auto-router", pricing: (model) => pricing[model] ?? null, evals: benchmarkEvals, }); ``` Pricing and output-token averages are optional here because the hosted Dari Auto Router owns its accounting. If you use self-hosted `createDariRoutingPolicy`, you must provide both yourself. The endpoint client posts to `/select` and accepts an endpoint that already ends in `/select`. ## Choosing an approach Use Auto Router when you want hosted selection but local execution. Use a local `RoutingPolicy` when a deterministic rule is enough or the conversation cannot leave your system. Use [`createDariRoutingPolicy`](/framework/policies) when you want the advanced self-hosted selector pipeline. Use the [Managed Router](/router/overview) when Dari should host the full service. # Custom Executors Source: https://docs.dari.dev/framework/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`. # Decision Pipeline Source: https://docs.dari.dev/framework/decision-pipeline Use the advanced policy-engine phases when selection needs orchestration. Most applications should use `createRouter`. Import `@mupt-ai/dari-router/policy-engine` only when your host must inspect routing evidence, call a selector itself, persist routing state, or coordinate selection with other work. The policy engine is the advanced deterministic core beneath [`createDariRoutingPolicy`](/framework/policies). It works with a different vocabulary than the framework: candidates are model/reasoning-level pairs keyed by `model` (not `id`), and inputs use wire-style names like `thinking_level`, `prefix_hits`, and `tool_choice_fp`. ## Prepare, select, finalize `prepareRoute()` performs deterministic candidate resolution and builds the selector request. Your host calls the selector. `finalizeRoute()` validates the selector output against the prepared candidates. ```ts theme={null} import { finalizeRoute, prepareRoute, type RouteInput, } from "@mupt-ai/dari-router/policy-engine"; const input: RouteInput = { candidateModels: ["openai/gpt-5.4-mini", "anthropic/claude-sonnet-4-6"], metadataLookup: (model) => modelMetadata[model]!, pricing: (model) => pricing[model] ?? null, requiredCapabilities: [], strategy: "slm", messages: conversation, chainsByModel: new Map([ ["openai/gpt-5.4-mini", []], ["anthropic/claude-sonnet-4-6", []], ]), prefixHits: [], nowMs: Date.now(), toolChoiceFp: "", responseFormatFp: "", selectorModel: "openai/gpt-5.4-mini", selectorContextWindowChars: 400_000, }; const prepared = prepareRoute(input); if (!prepared.selectorPreparation) throw new Error("No selector request"); const selectorOutput = await callSelector(prepared.selectorPreparation.selectorRequest); const result = finalizeRoute(prepared, selectorOutput); console.log(result.decision); ``` The input is deliberately explicit. The policy engine does not read credentials, environment variables, databases, or a clock. Supply model metadata, pricing, conversation state, and timestamps from your host. `route(input, selector)` composes the two phases when you do not need an interleaving point. Most fields have straightforward meanings. `chainsByModel` and `prefixHits` describe the conversation's prompt-cache history: which provider-cache blocks are already warm, so cost estimates can price a cached read instead of a full write. `toolChoiceFp` and `responseFormatFp` are fingerprints of the last tool-choice and response-format settings; a change there invalidates prompt-cache continuity, so the engine prices the tail differently. ## Evidence and state The prepared result exposes resolved model/reasoning pairs, cache-aware cost estimates, pruning decisions, recovered decisions, warnings, and the exact selector request. Unknown pricing remains unknown rather than becoming zero. Hosts can persist and later provide `chainsByModel`, `prefixHits`, and previous decisions. Those values describe provider-cache continuity (whether a provider can read previously seen prompt blocks from cache instead of paying for them again); they are not a replacement for application conversation storage. [Prompt Caching](/framework/prompt-caching) explains how this routing evidence differs from provider usage and selection leases. ## Selector output and fallback The selector returns JSON containing `selected_model`, `reasoning_effort`, and `reason`. `finalizeRoute()` requires the selected pair to be among prepared candidates. The prepared fallback configuration can produce a validated fallback decision; executing it and reporting usage remain host responsibilities. ## Anonymous selection The policy engine also supports training/evaluation flows where the selector input hides which real model each option is: candidates are replaced by shuffled action labels (for example `A`, `B`, `C`) so the selector scores the option, not the brand: ```ts theme={null} import { assignAnonymousActions, anonymizeSelectorInput, parseAnonymousActionSelection, } from "@mupt-ai/dari-router/policy-engine"; const mapping = assignAnonymousActions(candidates, Math.random); const anonymousInput = anonymizeSelectorInput(selectorInput, mapping); const selectedPair = parseAnonymousActionSelection(selectorOutput, mapping); ``` Use a seeded random source for reproducible datasets. Anonymous selection is for selector training and evaluation, not a requirement for ordinary routing. The complete no-network example is `dari-router/examples/basic_route.ts`. # Models Source: https://docs.dari.dev/framework/models Declare candidates and let the router filter them by capability. A model declaration is a candidate, not necessarily the model that will serve a request. Before calling your policy, the router filters out candidates that cannot satisfy the request. ## Models from Pi `pi.model()` reads the Pi model catalog and supplies provider, API, reasoning, and capability metadata: ```ts theme={null} import { createPiRuntime } from "@mupt-ai/dari-router"; const pi = await createPiRuntime({ apiKey: process.env.OPENAI_API_KEY! }); const models = [ pi.model("openai/gpt-5.4-mini"), pi.model("openai/gpt-5.4", { defaultReasoningEffort: "high" }), ]; ``` Use the model's full `provider/model-id` name. See [Pi Runtime](/framework/pi-runtime) for credentials and execution. ## Custom declarations For a model outside Pi's catalog, declare the metadata your policy and executor need: ```ts theme={null} import type { RouterModel } from "@mupt-ai/dari-router"; 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` annotation preserves the literal reasoning-level types in standalone declarations. Inline declarations are contextually typed. The executor name selects the implementation in `createRouter({ executors })`. If a model omits `executor`, `createRouter` uses its default `executor` option. A model without either is a configuration error. Named executors take precedence over the default. When omitted, `provider` defaults from the ID prefix, `api` defaults from the executor name, reasoning defaults to `off`, and capabilities default to false. Set metadata explicitly when those defaults are not true for your transport. ## Eligibility The policy sees only candidates that support the request. Filtering covers image input, tools and tool history, structured output, streaming, and explicit reasoning effort. If no candidate remains, the request fails before the policy runs. A policy cannot select an ineligible candidate. A request without a reasoning constraint may select any effort exposed by a candidate. A request with one is a hard constraint. Framework reasoning levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`; executors map them to provider-specific controls. # Operations Source: https://docs.dari.dev/framework/operations Run a reliable router across requests, replicas, and provider failures. `createRouter` handles request normalization, eligibility, selection, execution, protocol serialization, and optional fallback. Your host remains responsible for credentials, deployment, durable state, and application telemetry. ## Leases A policy can commit a model for later turns: ```ts theme={null} const policy: RoutingPolicy = ({ candidates }) => ({ model: candidates[0]!.id, leaseTurnsRemaining: 3, reason: "Keep this conversation on one model.", }); ``` A lease is used only when the request has a `cacheKey` (OpenAI or Anthropic `prompt_cache_key`). The default in-memory store expires leases after 30 minutes. `createRouter` calls `pruneExpired()` before each cacheKey-bearing selection, and every served turn refreshes the TTL, so leases serve while they still have turns and have not expired. `router.select()` and `router.fetch()` honor leases; `router.evaluatePolicy()` does not. A selection lease and a provider prompt cache are related but different: the lease keeps routing on one model, while the provider decides whether the prompt prefix is cached. See [Prompt Caching](/framework/prompt-caching) for the complete responsibility boundary. For replicas or restarts, provide a `LeaseStore` backed by your database or cache: ```ts theme={null} import type { LeaseStore } from "@mupt-ai/dari-router"; const leaseStore: LeaseStore = { async get(cacheKey) { return database.readLease(cacheKey); }, async set(cacheKey, lease) { await database.writeLease(cacheKey, lease); }, async delete(cacheKey) { await database.deleteLease(cacheKey); }, async pruneExpired(nowMs) { await database.deleteExpiredLeases(nowMs); }, }; ``` The store owns atomic read-modify-write behavior under concurrent requests. Store failures are advisory: a failed operation does not fail the request, but a lease may not be honored or persisted. ## Hooks Hooks are fire-and-forget and may return promises. A throwing or rejecting hook does not change the response: ```ts theme={null} const router = createRouter({ models, policy, executor, hooks: { onSelection(selection, request) { metrics.selection(selection, request); }, onCompletion(completion, selection) { metrics.completed(completion, selection); }, onStreamClose(completion, selection, error) { metrics.stream(selection, error); }, onError(error, selection) { logger.error(error, selection); }, }, }); ``` `onSelection` observes the selected decision. Completion and error hooks identify the model that actually served, including a fallback. ## Fallback Enable one cross-model retry at the router boundary: ```ts theme={null} const router = createRouter({ models, policy, executor, fallback: { enabled: true, requiresDifferentProvider: true }, }); ``` The router tries another eligible candidate if execution fails. Same-model retry budgets and provider error classification belong to the executor. A failed leased primary is released before the next turn. ## IDs and deployment Use `generateId` when tests or application tracing require deterministic response IDs. Deploy the `fetch` handler behind your web server or Bun's server. The router itself does not persist conversations or provider-cache observations. ## When to leave `createRouter` `createRouter` is selector-first. If you need durable routing state, speculative execution, cache-aware cost estimates, or custom billing/retry orchestration, use the lower-level `/policy-engine` phases described in [Decision Pipeline](/framework/decision-pipeline), [Prompt Caching](/framework/prompt-caching), and [Speculative Routing](/framework/speculative-routing). # Router Framework Source: https://docs.dari.dev/framework/overview Host one OpenAI- and Anthropic-compatible endpoint across multiple models. The Dari Router Framework is a small, pre-1.0 TypeScript package for putting several language models behind one web-standard `fetch` handler. You declare models, provide a policy that chooses among eligible candidates, and provide executors that call those models. ```ts theme={null} import { createRouter, type RoutingPolicy } from "@mupt-ai/dari-router"; const policy: RoutingPolicy = ({ candidates }) => ({ model: candidates[0]!.id, reason: "Use the first eligible model.", }); const router = createRouter({ models, policy, executor }); Bun.serve({ port: 3000, fetch: router.fetch }); ``` `models`, `policy`, and `executor` are the three things you supply; `models` is a list of candidate declarations, `executor` is the adapter that calls them, and the Quickstart shows all three together for a complete minimal server. `router.fetch` accepts OpenAI Chat Completions and Anthropic Messages requests and returns the matching response format. The framework does not host your credentials, persist your conversations, or choose a provider for you. ## The three decisions you make **Models** are candidate declarations. They describe an ID, reasoning levels, provider metadata, and capabilities such as images, tools, structured output, and streaming. **Policies** choose one eligible model. A policy can be a local function, a service you own, the self-hosted deterministic policy from `/policy-engine`, or Dari's hosted Auto Router. **Executors** run the selected model. The package includes `createPiRuntime()`, which calls models through the Pi model catalog with your credentials; you can also connect any SDK with a `RouterExecutor`. ## Framework or managed router? Use this package when your application should own the HTTP endpoint, provider credentials, execution, and operations. Use the [Dari Managed Router](/router/overview) when Dari should host those concerns. ## Public package boundaries The root package is the end-to-end framework: ```ts theme={null} import { createRouter, createPiRuntime, createAutoRouter } from "@mupt-ai/dari-router"; ``` Advanced deterministic routing is intentionally explicit: ```ts theme={null} import { prepareRoute, finalizeRoute } from "@mupt-ai/dari-router/policy-engine"; ``` Pure protocol adapters and continuation helpers are available from `@mupt-ai/dari-router/protocols`. Most applications should start with `createRouter` and never need either subpath. Pre-1.0 APIs may change. Do not build compatibility wrappers around removed options such as `createRouter({ runtime: ... })`; the generic router uses `executor`. Run a local router in a few minutes. Declare Pi or custom candidates. Choose a routing strategy. Handle leases, hooks, and fallback. # Pi Runtime Source: https://docs.dari.dev/framework/pi-runtime Execute catalog models with Dari's built-in provider adapter. `createPiRuntime()` is the first-party executor built on `@mupt-ai/pi-ai`. It turns the framework's normalized request into a provider call and converts the result back into the framework completion or stream contract. The runtime uses Pi's model catalog. Supported providers and APIs depend on the catalog version installed with the package. ## Supply credentials The host supplies credentials; the framework does not read environment variables. ```ts theme={null} const pi = await createPiRuntime({ apiKey: process.env.OPENAI_API_KEY!, }); ``` For multiple providers, resolve credentials per call: ```ts theme={null} const pi = await createPiRuntime({ apiKey: async ({ provider, model, api, purpose }) => { const key = await loadKey({ provider, model, api, purpose }); if (!key) throw new Error(`No credential for ${provider}`); return key; }, timeoutMs: 120_000, maxRetries: 2, }); ``` The resolver receives `provider`, `model`, `api`, and `purpose` (`execution` or `selector`). Keep credentials in your application; never put them in model declarations or requests. ## Connect it to a router ```ts theme={null} const router = createRouter({ executor: pi, models: [ pi.model("openai/gpt-5.4-mini"), pi.model("openai/gpt-5.4"), ], policy: ({ candidates }) => ({ model: candidates[0]!.id, reason: "Use the first eligible model.", }), }); ``` `executor: pi` is the default for model declarations that do not name an executor. Models can override it with `executor: "other"` and a matching `executors` entry. ## What is normalized The runtime supports the framework's portable messages, images, tools, tool results, reasoning, hosted web-search items, generation controls, response formats, prompt-cache correlation, streaming, and cancellation. Provider APIs do not all support every feature. Unsupported combinations fail as request or configuration errors instead of being silently dropped; see [Protocols And Streaming](/framework/protocols-and-streaming). Pi may adapt `json_object` to a permissive schema on providers without native JSON-object mode. Remote image URLs are not accepted by the shared image contract; download them in your host or use a custom executor. ## Retries and fallback `maxRetries` controls retries for the same model inside Pi. Cross-model fallback is configured on `createRouter`, not on the runtime: ```ts theme={null} const router = createRouter({ models, policy, executor: pi, fallback: { enabled: true, requiresDifferentProvider: true }, }); ``` See [Custom Executors](/framework/custom-executors#fallback) for the router-level behavior and [Prompt Caching](/framework/prompt-caching) for cache keys, provider usage, and cache-aware routing. # Policies Source: https://docs.dari.dev/framework/policies Choose an eligible model with a local function, selector, or hosted service. A policy receives the normalized request and the candidates that survived capability filtering. It returns a model ID and may choose a reasoning effort. ## A local policy ```ts theme={null} import type { RoutingPolicy } from "@mupt-ai/dari-router"; const policy: RoutingPolicy = ({ request, candidates }) => { const wantsTools = request.tools.length > 0; const preferred = wantsTools ? "anthropic/claude-sonnet-4-6" : "openai/gpt-5.4-mini"; const selected = candidates.find((candidate) => candidate.id === preferred) ?? candidates[0]!; return { model: selected.id, reasoningEffort: selected.defaultReasoningEffort, reason: wantsTools ? "This candidate supports the requested tools." : "Use the fast candidate.", }; }; ``` Policies may be synchronous or asynchronous. Router Core validates the returned model and reasoning effort. Omit `reasoningEffort` to use the candidate's default. The TypeScript field is `reasoningEffort`; wire JSON uses `reasoning_effort`. ## Inspect or serve a selection `router.evaluatePolicy(request)` is stateless: it runs the policy without reading or writing leases and without firing hooks. Use it for previews, logs, or tests. `router.select(request)` is the authoritative serving primitive. It reads an existing lease, applies a new lease returned by the policy, and fires `onSelection`. `router.fetch()` uses the same semantics. ```ts theme={null} import { openAIChatRequest } from "@mupt-ai/dari-router/protocols"; const request = openAIChatRequest({ model: "my-router", messages: [{ role: "user", content: "Explain cache locality." }], }); const preview = await router.evaluatePolicy(request); const servingSelection = await router.select(request); console.log(preview.decision, servingSelection.decision); ``` ## Self-hosted selector policy The advanced `createDariRoutingPolicy` uses a selector model (an LLM that answers routing questions) and the deterministic policy engine to assemble evidence, ask the selector, and validate its answer. It is public from `/policy-engine` because it is more opinionated than the core policy interface. ```ts theme={null} import { createPiRuntime, createRouter } from "@mupt-ai/dari-router"; import { createDariRoutingPolicy } from "@mupt-ai/dari-router/policy-engine"; const pi = await createPiRuntime({ apiKey: process.env.OPENAI_API_KEY! }); const policy = createDariRoutingPolicy({ runtime: pi, selectorModel: "openai/gpt-5.4-mini", selectorContextWindowChars: 400_000, pricing: (model) => myPricing[model] ?? null, averageOutputTokensByModel: myAverageOutputTokensByModel, }); const router = createRouter({ models: [pi.model("openai/gpt-5.4-mini"), pi.model("openai/gpt-5.4")], policy, executor: pi, }); ``` `runtime: pi` is valid here: it configures the selector used by `createDariRoutingPolicy`. It is not a `createRouter` option. For self-hosted `createDariRoutingPolicy`, provide your own pricing and observed output-token averages. Router Core does not ship or infer them. Cache state and model metadata remain optional; unknown cache state means the engine makes no cache-savings claim. ## Custom rules ```ts theme={null} const policy = createDariRoutingPolicy({ runtime: pi, selectorModel: "openai/gpt-5.4-mini", pricing: (model) => myPricing[model] ?? null, averageOutputTokensByModel: myAverageOutputTokensByModel, strategy: "custom", customConfig: { rules: [ { when: "architecture or difficult debugging", use: "openai/gpt-5.4", thinking_level: "high" }, { when: "simple factual questions", use: "openai/gpt-5.4-mini" }, ], default: "openai/gpt-5.4-mini", }, }); ``` The selector must return an eligible model and effort. For a fully local deterministic policy, write a `RoutingPolicy` directly; for full preparation/finalization control, use the [advanced policy engine](/framework/decision-pipeline). # Prompt Caching Source: https://docs.dari.dev/framework/prompt-caching Keep a warm model when switching would cost more than it saves. Cache-aware routing stops a policy from switching away from a warm model for a tiny apparent saving. Give the policy real prices and the conversation's recent cache state; it removes switches that do not pay for their cold prefix. ## Use It In Your Policy Use `createDariRoutingPolicy`, not a plain inline `RoutingPolicy`: ```ts theme={null} import { createPiRuntime, createRouter } from "@mupt-ai/dari-router"; import { createDariRoutingPolicy } from "@mupt-ai/dari-router/policy-engine"; const pi = await createPiRuntime({ apiKey: process.env.OPENAI_API_KEY! }); const policy = createDariRoutingPolicy({ runtime: pi, selectorModel: "openai/gpt-5.4-mini", selectorContextWindowChars: 400_000, // Your per-million-token provider prices, including cached-input prices. pricing: (model) => myPricing[model] ?? null, // Your observed output averages. Router Core does not provide these. averageOutputTokensByModel: myAverageOutputTokensByModel, // Read the state saved for this conversation after earlier provider calls. state: async ({ request }) => request.cacheKey ? cacheState.read(request.cacheKey) : {}, }); const router = createRouter({ models: [pi.model("openai/gpt-5.4-mini"), pi.model("openai/gpt-5.4")], policy, executor: pi, }); ``` `cacheState.read()` returns the state the policy engine needs: ```ts theme={null} { chainsByModel, // prompt-prefix chain visible to each candidate model prefixHits, // recently observed provider prefixes and the model that served them nowMs: Date.now(), } ``` After each provider call, save the updated prompt chain, provider-prefix observation, and selected model under the same key. That persistence is application code: Router Framework does not store conversations or cache observations for you. ## What You Get For a conversation with a warm incumbent, the policy engine compares each switch's cold-prefix cost with its expected savings. It automatically removes a switch unless it saves at least **10%** on the fixed-turn cost. So when `gpt-5.4-mini` looks a little cheaper but switching would reprocess a large warm prefix, it is removed before the selector chooses. A genuinely cheaper switch stays available. This protection works only when all of these are true: * The request has a stable `prompt_cache_key`. * Your saved state identifies the previous model and a still-warm prefix. * You provide normal and cached-input prices plus observed output-token averages for each model/effort. Router Core does not ship, infer, or persist output averages. Self-hosted users own those estimates; Dari's managed platform can maintain its own. If an input is missing or stale, the engine makes no cache-savings claim and does not prune on cache cost. ## Send The Same Key Every Turn Send `prompt_cache_key` on every request in one conversation: ```json theme={null} { "model": "my-router", "prompt_cache_key": "conversation-42", "messages": [{ "role": "user", "content": "Continue." }] } ``` Both the OpenAI Chat Completions and Anthropic Messages adapters accept this field. They normalize it to `request.cacheKey`; `createPiRuntime()` passes it to Pi as the provider session ID. Custom executors receive `request.cacheKey` and should map it to the cache or session field supported by their provider. Provider cache hits remain the provider's decision. Executors return observed usage as `cacheReadTokens` and `cacheWriteTokens`; the protocol adapters serialize those values in the caller's native usage format. ## Leases Are A Simpler Fallback A lease keeps later turns on one model, but it is not a cache-cost calculation or a cache hit. Use `leaseTurnsRemaining` when you only want short-term stickiness. Use cache-aware routing when you persist prefix observations and want the policy to decide whether a switch is actually worth it. Read [Policies](/framework/policies) for the full policy setup and [Decision Pipeline](/framework/decision-pipeline) if you need to control persistence and selection phases yourself. # Protocols And Streaming Source: https://docs.dari.dev/framework/protocols-and-streaming Use OpenAI Chat Completions and Anthropic Messages with one normalized router. `router.fetch()` accepts `POST` requests in either protocol and returns the same protocol in the response. | Protocol | Paths | | ----------------------- | ---------------------------------------------- | | OpenAI Chat Completions | `/v1/chat/completions` and `/chat/completions` | | Anthropic Messages | `/v1/messages` and `/messages` | Unknown paths return not-found errors. Known paths require `POST`. ## Pure request adapters The `/protocols` subpath exposes adapters without HTTP dispatch: ```ts theme={null} import { anthropicRequest, openAIChatRequest } from "@mupt-ai/dari-router/protocols"; const openAI = openAIChatRequest({ model: "my-router", messages: [{ role: "user", content: "Hello" }], }); const anthropic = anthropicRequest({ model: "my-router", max_tokens: 256, messages: [{ role: "user", content: "Hello" }], }); ``` Both become a `RouterRequest`. Its `items` preserve messages, tool calls, tool results, reasoning, and hosted tool calls; tools, generation controls, reasoning, response format, metadata, cache key, and streaming remain separate fields. The incoming model is `requestedModel`; it does not force the policy's selection. ## Responses and routing metadata The response includes a `dari_routing` object with the requested model, selected model, reasoning effort, and policy reason. Headers include `X-Router-Selected-Model` and, when available, `X-Router-Reasoning-Effort`. Streaming responses include routing metadata in their initial protocol event. ## Streaming Set `stream: true`. Router Core translates normalized executor events into the caller's SSE format and primes the stream before committing headers, so failures before provider output are ordinary JSON errors. After commitment, failures become protocol stream errors. Cancellation aborts the policy/executor signal and closes the executor iterator. OpenAI `stream_options` is accepted for compatibility; usage is emitted in the final usage chunk. Anthropic receives its normal message event sequence. ## Tools and images The portable contract supports function tools, tool choice, tool results, images, and structured output. The executor must advertise the relevant capability. Hosted `web_search` items are provider-native and replayable; OpenAI can serialize them, while Anthropic Messages cannot represent them and fails closed if one reaches Anthropic output. ## Reasoning and continuation Reasoning is normalized across providers. OpenAI responses use `reasoning_content` and `reasoning_details`; Anthropic responses use thinking blocks. Provider continuations are tagged with their source identity and are not replayed across incompatible providers. When readable reasoning has no provider continuation, the router wraps it in a portable `dari-ir-v1` envelope — a versioned, base64 JSON capsule that lets the next request carry prior reasoning without inventing provider-native signatures the provider could not parse. The `/protocols` entry point exposes protocol and continuation types for custom adapters. Preserve continuation fields when forwarding a request; do not invent provider signatures. ## Errors Framework errors are represented by `RouterFrameworkError` and serialized in the caller's protocol shape. Common boundaries are: | Kind | Meaning | | ----------------- | -------------------------------------------------- | | `invalid_request` | Malformed request or no eligible model | | `configuration` | Invalid router, model, executor, or protocol setup | | `policy` | Policy failure or invalid decision | | `executor` | Provider or output failure | | `cancelled` | Work aborted before response commitment | The framework implements a portable subset rather than every provider-specific feature. Unsupported fields are rejected instead of silently disappearing. # Quickstart Source: https://docs.dari.dev/framework/quickstart Build and run a router with a local policy and executor. This guide creates a router with no provider credentials. The local executor makes the example runnable while showing the same API used by a real executor. ## Install ```bash theme={null} mkdir my-router && cd my-router bun init -y bun add @mupt-ai/dari-router ``` The package is ESM and requires Node.js 22.19 or later, or Bun. ## Create `server.ts` ```ts theme={null} import { createRouter, type RouterExecutor, type RoutingPolicy, } from "@mupt-ai/dari-router"; const policy: RoutingPolicy = ({ request, candidates }) => { const text = request.items .filter((item) => item.type === "message") .flatMap((item) => item.content) .filter((part) => part.type === "text") .map((part) => part.text) .join(" "); const preferred = text.length > 80 ? "demo/strong" : "demo/fast"; const model = candidates.find((candidate) => candidate.id === preferred) ?? candidates[0]!; return { model: model.id, reason: `Selected ${model.id}.` }; }; const executor: RouterExecutor = { execute({ request, model, decision }) { const text = `${model.id} handled ${request.protocol}: ${decision.reason}`; return { type: "complete", output: { content: [{ type: "text", text }], finishReason: "stop", usage: { inputTokens: 1, outputTokens: 1 }, }, }; }, }; const router = createRouter({ models: [ { id: "demo/fast", executor: "demo", capabilities: { streaming: true } }, { id: "demo/strong", executor: "demo", capabilities: { streaming: true, toolUse: true, imageInput: true }, reasoningEfforts: ["off", "high"], }, ], policy, executors: { demo: executor }, }); Bun.serve({ port: 3000, fetch: router.fetch }); console.log("Listening on http://localhost:3000"); ``` Run it: ```bash theme={null} bun run server.ts ``` On Node.js rather than Bun, replace `Bun.serve` with your web framework's handler adapter and run `node server.ts` — the `router.fetch` is a standard `Request`-to-`Response` handler. ## Send a request ```bash theme={null} curl http://localhost:3000/v1/chat/completions \ -H 'content-type: application/json' \ -d '{"model":"my-router","messages":[{"role":"user","content":"Hello"}]}' ``` The response is OpenAI Chat Completions-shaped. The selected model is also reported in `dari_routing` and `X-Router-Selected-Model`. The caller's `model` value names your router; it does not force a candidate with that ID. The same handler accepts `POST /v1/messages` for Anthropic Messages. Add `stream: true` to either request to receive that protocol's SSE stream. ## Use real models next Replace the local executor with [`createPiRuntime`](/framework/pi-runtime), or keep your own transport and read [`Custom Executors`](/framework/custom-executors). Then choose a more capable policy from [`Policies`](/framework/policies). # Speculative Routing Source: https://docs.dari.dev/framework/speculative-routing Overlap a likely execution with an advanced policy selection. Normal `createRouter` behavior is selector-first: it selects a model, then executes it. Speculative routing starts a likely execution while the next route is being selected. This can reduce selector latency, but it makes state, cancellation, and reporting your responsibility. This is an advanced host pattern. The [Managed Router](/router/speculative-routing) provides its own hosted implementation; `createRouter` does not speculate automatically. ## Why use the phases? `prepareRoute()` resolves candidates and prepares selector input. Once you have that result, your host can start a previously selected, still-compatible model. `finalizeRoute()` validates the selector's next decision. ```ts theme={null} import { finalizeRoute, prepareRoute } from "@mupt-ai/dari-router/policy-engine"; const prepared = prepareRoute(routeInput); if (!prepared.selectorPreparation) throw new Error("Selector unavailable"); const incumbent = prepared.previousDecision; const stillEligible = incumbent !== undefined && prepared.candidateResolution.candidates.some((candidate) => candidate.model === incumbent.model && candidate.reasoningEffort === incumbent.reasoningEffort ); const speculative = stillEligible ? startModel(incumbent) : null; const selectorOutput = await callSelector(prepared.selectorPreparation.selectorRequest); const next = finalizeRoute(prepared, selectorOutput); const served = await serveOrRecover({ speculative, selectedRoute: next.decision }); await persistNextDecision({ served, nextDecision: next.decision }); ``` `startModel`, `callSelector`, `serveOrRecover`, and `persistNextDecision` are application functions. Router Core does not know how to start, cancel, persist, or bill them. ## Eligibility is mandatory Speculate only when the recovered model and reasoning level remain eligible for the current request. A changed tool set, image requirement, structured-output mode, stream mode, reasoning constraint, or routing rule can invalidate the incumbent. First turns and stale state should remain selector-first. ## Separate the two decisions The model serving this turn can differ from the decision selected for the next turn. Attribute tokens, latency, errors, and billing to the model that served. Persist the next decision separately. Do not report a speculative incumbent as if it were the selector's result. ## Failure policy Define explicit behavior for every race: * If speculative startup fails, await the selected route. * If selection fails after startup, decide whether the incumbent may finish. * If the client disconnects, cancel both operations and close provider streams. * Persist routing state before emitting a terminal event when durable state is required. If these rules are not worth owning, use selector-first `createRouter` instead. # Dari Overview Source: https://docs.dari.dev/index Use Dari's managed router or build and self-host your own. Dari provides two ways to route model requests. The **managed platform** gives applications and coding agents a hosted endpoint with configured models, provider credentials, persistent routing state, retries, fallbacks, telemetry, and speculative routing. Start with the default `/v1` router and no router setup. The **Router Framework** is a TypeScript package for building and self-hosting your own endpoint. You provide models, policies, credentials, and operational infrastructure; the framework handles normalization, eligibility, provider execution, and protocol translation. Send your first request through Dari's hosted default router. Build and self-host a router with pluggable policies. Understand model eligibility, selection, and routing state. Run Router Core locally and create a handler. Run Claude Code, Codex, or Pi through the managed router. View the hosted HTTP endpoints. # Install the Dari CLI Source: https://docs.dari.dev/install-cli Install the CLI for managing Dari routers from your terminal. Install the Dari CLI on macOS or Linux: ```bash theme={null} curl -fsSL https://raw.githubusercontent.com/mupt-ai/dari-cli/main/install.sh | bash ``` Then sign in: ```bash theme={null} dari auth login ``` The CLI can create routers from YAML, inspect router details, manage API keys, and send requests. It can also configure and launch installed coding agents through your default router while forwarding their native arguments: ```bash theme={null} dari --claude dari --codex dari --pi ``` Run `dari --help` to see all available commands. Run `dari --skill` to print the bundled managed-router instructions for a coding agent. * [Use Claude Code](/router/coding-agents/claude-code) * [Use Codex](/router/coding-agents/codex) * [Use Pi](/router/coding-agents/pi) * [Router Quickstart](/router/quickstart) * [Create A Router](/router/create-a-router) * [CLI Repository](https://github.com/mupt-ai/dari-cli) # Claude Code Source: https://docs.dari.dev/router/coding-agents/claude-code Run Claude Code through a Dari Router. Install the [Dari CLI](/install-cli), then launch Claude Code through your organization's default router: ```bash theme={null} dari --claude ``` On first use, Dari signs you in if needed, creates and privately caches a Routing key, and starts the installed `claude` executable with the Dari endpoint and `dari/routing` model. Your existing Claude Code configuration is left intact. The launcher also offers to use your Claude Code personal subscription. Personal subscriptions require your organization's Pro plan: without it, the launcher uses Dari managed billing, and a connected subscription stays saved under **Credentials** until the organization starts Pro. Every following argument is forwarded to Claude Code: ```bash theme={null} dari --claude --print "Review this diff" dari --claude --dangerously-skip-permissions ``` Claude Code sends Anthropic Messages requests to `/v1/messages`. The launcher prevents a saved effort setting from overriding the router and pins subagents to `dari/routing`, so delegated work stays on the router. The default router chooses from its current managed model set, including a reasoning level for each request. Check **Activity** in the dashboard to see each selection. Claude Code's background calls—auto mode's safety checks and conversation titles—skip selection entirely and run on the cheapest model your router has enabled. They are short classifications, so routing them would cost more than it saves. Set `DARI_ROUTING_API_KEY` before launching to use an existing Routing key instead of the CLI-managed key. ## Manual Configuration If you do not want to use the launcher, create a [Routing API key](/api-keys#create-a-routing-key) and configure the same environment yourself: ```bash theme={null} export DARI_ROUTING_API_KEY="dari_..." ANTHROPIC_BASE_URL="https://routing.dari.dev" \ ANTHROPIC_AUTH_TOKEN="$DARI_ROUTING_API_KEY" \ ANTHROPIC_API_KEY="" \ CLAUDE_CODE_EFFORT_LEVEL="auto" \ CLAUDE_CODE_MAX_CONTEXT_TOKENS="1000000" \ CLAUDE_CODE_SUBAGENT_MODEL="dari/routing" \ claude --model dari/routing ``` `--model dari/routing` selects the router for the main session and `CLAUDE_CODE_SUBAGENT_MODEL="dari/routing"` pins subagents to it. Routing `dari/routing` selects the router's model set—it does not force one particular upstream model. The router picks the model and reasoning level per request. To use a different model set, edit the default router or [Create A Router](/router/create-a-router) with public or [Custom Models](/router/configure-models#custom-models). Set that router as the default to keep this same launcher, or use the manual configuration and point `ANTHROPIC_BASE_URL` at `https://routing.dari.dev/{router_id}`. # Codex Source: https://docs.dari.dev/router/coding-agents/codex Run Codex through a Dari Router. Install the [Dari CLI](/install-cli), then launch Codex through your organization's default router: ```bash theme={null} dari --codex ``` On first use, Dari signs you in if needed, creates and privately caches a Routing key, and starts the installed `codex` executable with invocation-local Dari provider settings. Your existing Codex configuration is left intact. A personal OpenAI subscription connected under **Credentials** serves your attributed requests only when the organization is on the Pro plan; otherwise the router's own provider keys are used. Every following argument is forwarded to Codex: ```bash theme={null} dari --codex exec "Fix the failing tests" dari --codex --sandbox workspace-write ``` Codex sends Responses requests through your organization's default router at `/v1`. The launcher selects `dari/routing` and disables unsupported reasoning summaries. Codex may still copy a global reasoning-effort preference into the request, but Dari treats that model-specific preference as automatic so the router can choose the reasoning level per request. Check **Activity** in the dashboard to see each selection. Set `DARI_ROUTING_API_KEY` before launching to use an existing Routing key instead of the CLI-managed key. ## Manual Configuration If you do not want to use the launcher, create a [Routing API key](/api-keys#create-a-routing-key): ```bash theme={null} export DARI_ROUTING_API_KEY="dari_..." ``` With Codex 0.134.0 or later, create `~/.codex/dari.config.toml`. The `dari` filename matches the profile name used below: ```toml theme={null} model = "dari/routing" model_provider = "dari" model_reasoning_summary = "none" [model_providers.dari] name = "Dari" base_url = "https://routing.dari.dev/v1" env_key = "DARI_ROUTING_API_KEY" wire_api = "responses" ``` ```bash theme={null} codex --profile dari ``` To use a different model set, edit the default router or [Create A Router](/router/create-a-router) with public or [Custom Models](/router/configure-models#custom-models). Set that router as the default to keep this same launcher, or use the manual configuration and point `base_url` at `https://routing.dari.dev/{router_id}/v1`. # Pi Source: https://docs.dari.dev/router/coding-agents/pi Run Pi through a Dari Router. Install the [Dari CLI](/install-cli), then launch Pi through your organization's default router: ```bash theme={null} dari --pi ``` On first use, Dari signs you in if needed, creates and privately caches a Routing key, and starts the installed `pi` executable with an invocation-local Dari provider extension. Your existing Pi configuration is left intact. Every following argument is forwarded to Pi: ```bash theme={null} dari --pi -p "Review this repository" dari --pi --tools read,grep,find,ls ``` Pi sends Chat Completions requests through your organization's default router at `/v1`. The launcher configures a one-million-token context window and session-affinity headers, which keep long sessions from compacting too early and group all requests from one session under the same conversation in **Activity**. Dari chooses the model and reasoning level per request. Set `DARI_ROUTING_API_KEY` before launching to use an existing Routing key instead of the CLI-managed key. ## Manual Configuration If you do not want to use the launcher, create a [Routing API key](/api-keys#create-a-routing-key): ```bash theme={null} export DARI_ROUTING_API_KEY="dari_..." ``` Add this provider to `~/.pi/agent/models.json`: ```json theme={null} { "providers": { "dari": { "baseUrl": "https://routing.dari.dev/v1", "api": "openai-completions", "apiKey": "$DARI_ROUTING_API_KEY", "compat": { "sendSessionAffinityHeaders": true }, "models": [ { "id": "dari/routing", "name": "Dari Router", "reasoning": false, "contextWindow": 1000000, "maxTokens": 128000 } ] } } } ``` ```bash theme={null} pi --provider dari --model dari/routing ``` `"reasoning": false` keeps Pi from sending a fixed reasoning level; Dari still chooses one per request. `"contextWindow": 1000000` matches the window shared by every model in the default router. `"sendSessionAffinityHeaders": true` gives Dari a stable session identifier even after compaction rewrites message history. To use a different model set, edit the default router or [Create A Router](/router/create-a-router) with public or [Custom Models](/router/configure-models#custom-models). For a specific router, use manual configuration, point `baseUrl` at `https://routing.dari.dev/{router_id}/v1`, and set `contextWindow` to the smallest window among its enabled models. Query its reported value with: ```bash theme={null} curl -H "Authorization: Bearer $DARI_ROUTING_API_KEY" \ https://routing.dari.dev/{router_id}/v1/models ``` # Configure Models Source: https://docs.dari.dev/router/configure-models Choose the models and reasoning levels a router may select. Add models with `enabled_models`: ```yaml theme={null} enabled_models: - openai/gpt-5.6-sol - anthropic/claude-sonnet-5 ``` Run `dari router models` or use the dashboard picker to see the current catalog. Each canonical model ID (e.g. `openai/gpt-5.6-sol`, `deepseek-ai/DeepSeek-V4-Flash-0731`) is provider-independent. By default Dari uses each model's default provider from the catalog (`dari router models` shows it), but you can bind a model to an alternate provider such as OpenRouter with `model_providers`: ```yaml theme={null} enabled_models: - openai/gpt-5.6-sol - xai/grok-4.6 model_providers: openai/gpt-5.6-sol: openrouter xai/grok-4.6: openrouter ``` The resolved provider determines which [Provider Credential](/router/configure-provider-keys) Dari uses. OpenRouter, xAI, and Amazon Bedrock are BYOK-only. Supported OpenAI and Claude models can also be served through Amazon Bedrock: ```yaml theme={null} enabled_models: - openai/gpt-5.6-sol - anthropic/claude-sonnet-5 model_providers: openai/gpt-5.6-sol: amazon-bedrock anthropic/claude-sonnet-5: amazon-bedrock ``` Bedrock mappings use `global.` cross-region inference profiles by default. With [AWS IAM authentication](/router/configure-provider-keys#amazon-bedrock), the configured AWS Region selects the Bedrock endpoint and SigV4 signing region. A region embedded in an inference-profile ARN takes precedence. Custom models can bind a regional profile or ARN when you need a specific inference profile. ## Reasoning Levels A router may use every reasoning level supported by a model unless you restrict it: ```yaml theme={null} model_thinking_levels: openai/gpt-5.6-sol: [low, medium, high] anthropic/claude-sonnet-5: [low, medium, high] ``` Supported levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. ## Custom Models To add an OpenAI-compatible model outside the public catalog, open **Models** in the [Dari Dashboard](https://app.dari.dev) and select **Add Model**. Enter its base URL, model ID, context limit, pricing, and provider credential. Custom models are private to your organization and use the credential saved with the model. # Configure Provider Credentials Source: https://docs.dari.dev/router/configure-provider-keys Save provider authentication once, then select it for routers and custom models. Provider credentials belong to your organization, not to an individual router. Save each API key or AWS IAM credential once on the **Credentials** page, then select it anywhere that provider is used. Each built-in router provider uses one of two sources: * **Managed** uses Dari's provider account when the provider supports it. * **Saved Credential** uses an organization credential you created for that provider. OpenRouter, xAI, and Amazon Bedrock are BYOK-only. Custom models also select a saved credential for their provider. ## Save An API Key In the dashboard, open **Credentials**, find the provider section, and choose **Add**. The value is write-only after save. You can also use the CLI without putting the secret in shell history: ```bash theme={null} printf '%s' "$OPENROUTER_API_KEY" | \ dari credentials provider add openrouter "OpenRouter Production" --value-stdin ``` The response includes a stable credential ID such as `cred_...`. Select it in a router manifest: ```yaml theme={null} name: Production Router enabled_models: - openrouter/openai/gpt-5.6-sol provider_credential_ids: openrouter: cred_... routing_strategy: slm ``` Or pass it directly: ```bash theme={null} dari router create "Production Router" \ --model openrouter/openai/gpt-5.6-sol \ --provider-credential openrouter=cred_... ``` ## Amazon Bedrock Amazon Bedrock supports two saved authentication types: * **API Key** uses a [Bedrock API key](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html), equivalent to `AWS_BEARER_TOKEN_BEDROCK`. Its IAM identity needs `bedrock:CallWithBearerToken` permission. * **AWS IAM** signs each Bedrock request with AWS Signature Version 4. Save an access key ID, secret access key, optional session token, and AWS Region. Create an AWS IAM credential from environment variables: ```bash theme={null} dari credentials provider add amazon-bedrock "Bedrock Production" \ --aws-region us-east-1 \ --aws-access-key-id-env AWS_ACCESS_KEY_ID \ --aws-secret-access-key-env AWS_SECRET_ACCESS_KEY \ --aws-session-token-env AWS_SESSION_TOKEN ``` Omit `--aws-session-token-env` for long-lived credentials. The IAM identity must be allowed to invoke every selected model or inference profile. Role assumption, instance profiles, and ambient credential discovery are not supported for per-router authentication. Public API responses expose only non-secret metadata, including the authentication type and AWS Region. Dari does not install saved values in process-wide environment variables or use ambient AWS profiles. ## Update Or Delete Updating preserves the credential ID, so every router and custom model using it receives the new value without being reconfigured: ```bash theme={null} printf '%s' "$OPENROUTER_API_KEY" | \ dari credentials provider update cred_... "OpenRouter Production" --value-stdin ``` Delete an unused credential with `dari credentials provider remove cred_...`. Dari rejects deletion while a router or custom model still references it. When a model is bound to OpenRouter via `model_providers`, Dari forwards strict zero-data-retention routing controls. Your application still sends only its Dari Routing API key; Dari resolves the saved provider credential after selecting a model. # Create A Router Source: https://docs.dari.dev/router/create-a-router Create an endpoint that routes across your enabled models. Create a router from a local YAML manifest with the Dari CLI. This is the easiest way to keep a router configuration in source control. ## YAML Create `router.yml`: ```yaml theme={null} name: Production Router enabled_models: - openai/gpt-5.6-sol - anthropic/claude-sonnet-4-6 provider_key_sources: openai: managed anthropic: managed routing_strategy: slm ``` Sign in and create the router: ```bash theme={null} dari auth login dari router create ./router.yml ``` `slm` enables automatic routing. For BYOK providers, first [save an organization provider credential](/router/configure-provider-keys), then reference its stable ID with `provider_credential_ids`. The CLI uses the platform default and enables speculative routing. The command returns a router ID. Use it to retrieve the endpoint: ```bash theme={null} dari router get ``` ## Dashboard For a quick setup, open **Routers** in the [Dari Dashboard](https://app.dari.dev), select **Create Router**, choose your models and saved provider credentials, and copy the endpoint from the router detail page. The dashboard is also where you can disable speculative routing. A router endpoint looks like this: ```text theme={null} https://routing.dari.dev/rtr_123/chat/completions ``` ## Next Steps * [Configure Models](/router/configure-models) * [Configure Provider Credentials](/router/configure-provider-keys) * [Speculative Routing](/router/speculative-routing) * [Router Evals](/evals/overview) # Activity Overview Source: https://docs.dari.dev/router/model-activity Inspect routed usage, spend, savings, tokens, and cache activity from the CLI. Use `dari activity` to read routing activity for your current organization. Commands print JSON so you can inspect them directly or pipe them into `jq`. Every report requires an RFC 3339 time range: ```bash theme={null} dari activity overview \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z ``` The overview includes message and model-step counts, provider spend, estimated savings, token and cache usage, model mix, key sources, and API-key activity. ## Find Filter IDs List the users, API keys, routers, models, and providers available in a time range: ```bash theme={null} dari activity filter-options \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z ``` Extract user IDs with `jq`: ```bash theme={null} dari activity filter-options \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z | jq '.users[] | {id, name, email}' ``` Pass the returned `id` to `--user-id` or `--comparison-user-id`. ## Common Filters Activity commands support these filters: ```bash theme={null} --router-id --api-key-id # repeatable --user-id # repeatable --model # repeatable --provider --status --key-source --source-scope ``` Statuses are `completed`, `provider_error`, `selector_error`, and `aborted`. `--source-scope` accepts a comma-separated protocol list, `all`, or `none`. Time-series reports choose a bucket size from the range: 60s, 5m, 15m, 30m, 1d, 7d, or 30d buckets. Override it with `--bucket-seconds` (one of `60`, `300`, `900`, `1800`, `86400`, `604800`, or `2592000`): ```bash theme={null} dari activity overview \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z \ --bucket-seconds 86400 ``` A range cannot exceed 366 days, and a report cannot span more than 1000 buckets. Choose a larger `--bucket-seconds` for long ranges. ## Authentication Sign in with `dari auth login` or set `DARI_API_KEY` to a Management key. Routing keys cannot read activity. With browser login, pass `--organization-id` to read an organization other than the current one. Every command maps to a [routing activity endpoint](/api-reference/managed/activity) on the [management API](/api-reference/managed/overview); use either surface. ## Reports * [Models & Routing](/router/model-activity/models-routing) * [People & Keys](/router/model-activity/people-keys) * [Conversations](/router/model-activity/conversations) * [Tools & Skills](/router/model-activity/tools-and-skills) # Conversations Source: https://docs.dari.dev/router/model-activity/conversations List conversations and inspect their routed steps from the CLI. List routed conversations: ```bash theme={null} dari activity conversations \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z ``` The response includes conversation-level messages, model steps, model switches, spend, tokens, and latest activity. ## Search And Sort ```bash theme={null} dari activity conversations \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z \ --search checkout \ --sort-by model_switches \ --sort-direction desc \ --limit 25 ``` Sort fields are `last_active`, `messages`, `model_steps`, `model_switches`, `spend`, and `tokens`. Use `--offset` for pagination. ## Inspect One Conversation Copy a `conversation_ref` from the list response, then inspect its routed steps: ```bash theme={null} dari activity conversations get ``` Use `--limit` and `--offset` to paginate long conversations: ```bash theme={null} dari activity conversations get \ --limit 100 \ --offset 100 ``` Conversation detail includes the selected model and provider for each step, usage and cost, completion state, route changes, and retained-payload availability. # Models & Routing Source: https://docs.dari.dev/router/model-activity/models-routing Compare model usage, cost, latency, outcomes, and route changes from the CLI. Run the model activity report: ```bash theme={null} dari activity models \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z ``` The JSON response includes: * Model steps and provider allocation. * Provider spend and cost per step. * Estimated savings against the most expensive enabled model. * Provider latency and non-completion rates. * Model switches and cross-provider transitions. Filter the report to investigate a route or failure mode: ```bash theme={null} dari activity models \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z \ --router-id rtr_123 \ --model openai/gpt-5.6-sol \ --status provider_error ``` Use `jq` to create a compact table: ```bash theme={null} dari activity models \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z | jq '.models[] | { model, model_steps, provider_spend_usd, provider_latency_p95_ms, non_completion_rate }' ``` Unknown pricing is not treated as free usage. Check priced and unpriced step counts before comparing spend across models. # People & Keys Source: https://docs.dari.dev/router/model-activity/people-keys List routed activity by attributed person and API key from the CLI. Find user IDs before filtering or comparing people: ```bash theme={null} dari activity filter-options \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z | jq '.users[] | {id, name, email}' ``` Then list activity attributed to people and API keys: ```bash theme={null} dari activity people \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z ``` Each identity includes its conversation, message, token, spend, key, and latest-activity data. ## Search And Scope Search identities or limit the report to people or keys: ```bash theme={null} dari activity people \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z \ --scope keys \ --search production ``` `--scope` accepts `all`, `people`, or `keys`. ## Sort And Paginate ```bash theme={null} dari activity people \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z \ --sort-by spend \ --sort-direction desc \ --limit 25 \ --offset 0 ``` Sort fields are `identity`, `keys`, `conversations`, `messages`, `tokens`, `spend`, and `last_active`. ## Compare People Over Time Compare attributed users with a time series: ```bash theme={null} dari activity people series \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z \ --comparison-user-id usr_123 \ --comparison-user-id usr_456 ``` Use `--bucket-seconds` to change the bucket size. Requests without usable person or API-key attribution still contribute to model totals, but appear as unattributed activity here. # Tools & Skills Source: https://docs.dari.dev/router/model-activity/tools-and-skills Inspect observed tool and skill usage from the CLI. Tools and skills use parallel command sets. ## Usage Reports Show totals, usage share, top capabilities, and time series: ```bash theme={null} dari activity tools \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z dari activity skills \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z ``` Select capabilities for the returned time series with repeatable `--series-id` flags: ```bash theme={null} dari activity tools \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z \ --series-id web_search \ --series-id shell ``` ## List Capabilities ```bash theme={null} dari activity tools list \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z \ --search search \ --sort-by uses \ --limit 25 dari activity skills list \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z ``` Sort fields are `name`, `uses`, and `latest`. Use `--sort-direction`, `--limit`, and `--offset` to control the result. ## Inspect One Capability Copy a `capability_id` from the list response: ```bash theme={null} dari activity tools get \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z dari activity skills get \ --from 2026-07-01T00:00:00Z \ --to 2026-07-08T00:00:00Z ``` The detail response includes usage, latest activity, identities, versions, sources, and a time series. # How Routing Works Source: https://docs.dari.dev/router/overview Understand Dari's managed router and default endpoint. Dari gives your application one hosted endpoint for multiple models. For each request, the router chooses an eligible model and reasoning level, calls the provider, and returns the response in the protocol you sent. Dari manages authentication, provider execution, routing state, retries, fallbacks, telemetry, and billing. ## Default Router Every organization has a default router at `/v1`. It works immediately with Dari-managed provider credentials, and its model set can change over time. The default router accepts: * [Chat Completions API](/router/send-chat-completions) * [OpenAI Responses API](/router/send-responses) * [Anthropic Messages API](/router/send-anthropic-messages) Create another router when you need a different model set, provider keys, routing rules, or evals. You can use its `rtr_...` endpoint directly or make it the new default. ## Router Selection A router first removes models that cannot handle the request—for example, models without the required tool, image, streaming, or reasoning support. Its routing policy then chooses among the remaining model and reasoning-level pairs. New routers use [Speculative Routing](/router/speculative-routing) by default to reduce latency on eligible follow-up turns. ## Self-Hosting Use the [Router Framework](/framework/overview) when you want to host the endpoint and own provider credentials, routing policies, state, and operations yourself. # Router Quickstart Source: https://docs.dari.dev/router/quickstart Send your first request through Dari's default model router. Send your first routed request with a Routing API key. You do not need to create or configure a router first. ## 1. Create A Routing Key Open **API Keys** in the [Dari Dashboard](https://app.dari.dev) and create a **Routing** key. ```bash theme={null} export DARI_ROUTING_API_KEY="dari_..." ``` ## 2. Send A Request The default `/v1` router uses Dari-managed provider credentials. Its model set is managed by Dari and may change over time. ```bash theme={null} curl "https://routing.dari.dev/v1/chat/completions" \ -H "Authorization: Bearer $DARI_ROUTING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "dari/routing", "messages": [ {"role": "user", "content": "Hello from Dari"} ] }' ``` The response appears in your terminal. Its `dari_routing.selected_model` field shows which model handled the request. ## Use Different Or Custom Models The default router's model set can be edited in the dashboard. You can also [Create A Router](/router/create-a-router) with different models and set it as the new default for `/v1`, or use its `rtr_...` endpoint directly. Add [Custom Models](/router/configure-models#custom-models) or [Custom Provider Keys](/router/configure-provider-keys) to any router. ## If The Request Fails A `401` usually means the Routing key is invalid. For a specific router, it can also mean the key and router belong to different organizations. For other router errors, verify the copied endpoint and [Provider Key Setup](/router/configure-provider-keys). ## Next Steps * [Chat Completions API](/router/send-chat-completions) * [OpenAI Responses API](/router/send-responses) * [Anthropic Messages API](/router/send-anthropic-messages) * [Use Claude Code](/router/coding-agents/claude-code) * [Use Codex](/router/coding-agents/codex) * [Use Pi](/router/coding-agents/pi) * [Create A Router](/router/create-a-router) # Anthropic Messages API Source: https://docs.dari.dev/router/send-anthropic-messages Send Anthropic Messages requests through a router. ```bash theme={null} curl "https://routing.dari.dev/v1/messages" \ -H "Authorization: Bearer $DARI_ROUTING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "dari/routing", "max_tokens": 256, "messages": [{"role": "user", "content": "Hello from Dari"}] }' ``` Use `/v1` for your default router. For a specific router, send the same body to `https://routing.dari.dev/{router_id}/messages`. The response follows the Anthropic Messages format and adds `dari_routing`, which identifies the selected model and reasoning effort. ## Streaming And Tools Set `stream: true` for Server-Sent Events. Tools use Anthropic's standard `tools`, `tool_choice`, `tool_use`, and `tool_result` shapes. See the [Anthropic Messages API Reference](/api-reference/router/messages) for supported fields. # Chat Completions API Source: https://docs.dari.dev/router/send-chat-completions Send OpenAI-compatible Chat Completions requests through a router. ```bash theme={null} curl "https://routing.dari.dev/v1/chat/completions" \ -H "Authorization: Bearer $DARI_ROUTING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "dari/routing", "messages": [{"role": "user", "content": "Hello from Dari"}] }' ``` Use `/v1` for your default router. For a specific router, send the same body to `https://routing.dari.dev/{router_id}/chat/completions`. The response follows the OpenAI Chat Completions format and adds `dari_routing`, which identifies the selected model and reasoning effort. ## Streaming Set `stream: true`. Dari returns Server-Sent Events in the OpenAI stream format, with `dari_routing` in the first event and `data: [DONE]` at the end. ## Tool Calls Send standard OpenAI `tools` and `tool_choice` fields. If the selected model calls a tool, append the result as a `tool` message and send the complete message history again. See the [Chat Completions API Reference](/api-reference/router/chat-completions) for supported fields. # OpenAI Responses API Source: https://docs.dari.dev/router/send-responses Send OpenAI Responses requests through a router. ```bash theme={null} curl "https://routing.dari.dev/v1/responses" \ -H "Authorization: Bearer $DARI_ROUTING_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "dari/routing", "input": "Hello from Dari" }' ``` Use `/v1` for your default router. For a specific router, send the same body to `https://routing.dari.dev/{router_id}/v1/responses`. The response follows the OpenAI Responses format and adds `dari_routing`, which identifies the selected model and reasoning effort. ## Streaming And Tools Set `stream: true` for HTTP streaming. Function, custom, and namespace tools use the standard Responses request shape. WebSockets and `previous_response_id` are not supported; resend the complete input history on each turn. See the [Responses API Reference](/api-reference/router/responses) for supported fields. # Speculative Routing Source: https://docs.dari.dev/router/speculative-routing Reduce follow-up latency by selecting the next route alongside the current response. Speculative routing reduces follow-up latency by continuing on the previous compatible route while Dari selects the route for the next turn. ```text theme={null} Current follow-up ├── Continue previous compatible model ──► response └── Select the next route ───────────────► saved for next turn ``` The selected-model metadata always identifies the model that served the current response. If the selector chooses a different route, that choice applies to the next turn. ## When It Applies Dari speculates only when it can recover a previous decision and that model and reasoning level can still serve the current request. Otherwise, it selects first and then calls the provider normally. A request remains selector-first when it is the first turn, the previous route is unavailable, or the current request requires capabilities the previous route cannot provide. ## Failures If the speculative provider call fails, Dari waits for selection and uses the router's normal retry and fallback settings. If selection fails after a valid speculative call has started, the current response can continue on that route. Usage and activity are always attributed to the model that actually served the request. ## Configuration New routers enable speculative routing by default. Open the router in the [Dari Dashboard](https://app.dari.dev) to switch it on or off. For a self-hosted implementation, see [Framework Speculative Routing](/framework/speculative-routing).