daari — Roadmap v3: ahead of OpenRouter¶
Status: Active — supersedes ROADMAP-v2.md for what to build next
Date: 2026-08-26
Trigger: Stripe agreed to acquire OpenRouter (terms undisclosed; Reuters reported ~$8B). Agent traffic on that network now burns ~5× the tokens of a human on the same model.
Sources: Stripe / OpenRouter, agent token economics, OpenRouter provider routing, ZDR, BYOK, live daari benches on Apple M4 Pro (Ollama 0.18.2).
v2 already shipped the gateway-parity trains (Responses API, virtual keys, Redis/Helm, guardrails). The remaining v2 leftovers are real but they are not why we are behind OpenRouter. This document is the competitive plan.
1. The category error¶
OpenRouter is a hosted marketplace: one key, 400+ models, 80+ providers, prepaid credits, live endpoint telemetry, Stripe-grade billing. Stripe bought that because tokens are becoming a unit of commerce (usage, credits, cached vs generated, per-model price tables).
daari is a local-first execution router: cache → rules → tools → small local models → frontier last. We do not have 10 million developers or 10 trillion tokens/day. We will not win by cloning a catalog Stripe just paid billions for.
We lose today because Cursor / Claude Code / agents already talk like OpenRouter customers (one OpenAI-shaped URL, tools on every turn, huge system prompts) and we refuse the cache on exactly that traffic (ADR-0004: agent turns skip L0). OpenRouter’s own data says >85% of agent token burn is re-reading cached prompts. That is our product, and we turn it off for agents.
| OpenRouter | daari today | |
|---|---|---|
| Default inference | Remote, paid | On-device, $0 if it hits L0–L5 |
| Catalog | 400+ models / 80+ providers | Ollama + optional L6 list |
| Same-model provider pick | Price / latency / throughput, live | Single base_url per slot |
| Failover | Outage, 429, context, moderation | Bounded HTTP retries + circuit breaker |
| Privacy controls | zdr, data_collection, EU pin |
Local-by-default; no ZDR object on L6 |
| Agent cache | Provider prompt-cache (billed cheaper) | L0/L1 skipped when tools present |
| Billing | Credits, BYOK, native tokenizers, usage.cost |
Ledger + gpt-4o implied price |
| Distribution | Hosted, one key, playground | daari serve on the laptop |
If we try to “be OpenRouter,” we lose. If we make agent turns cheaper than OpenRouter by not leaving the machine, we are the product Stripe cannot buy.
2. What OpenRouter can do that we cannot¶
Request-body provider object (Chat Completions):
| Field | What it does | daari |
|---|---|---|
order / only / ignore |
Pin or skip provider slugs | Partial: ordered frontier.providers in config, not per-request |
allow_fallbacks |
Backup endpoints when primary dies | Circuit breaker on slots; no request flag |
sort: price | throughput | latency |
Reorder live endpoints | No live endpoint telemetry |
preferred_min_throughput / preferred_max_latency |
p50–p99 cuts | No |
max_price |
Cap $/M tokens | Daily/monthly USD budget only |
require_parameters |
Skip endpoints that cannot honor tools / max_tokens | Capability catalog is local-only |
data_collection |
Deny providers that store/train | No |
zdr |
Zero Data Retention endpoints only | No |
quantizations |
int4 / int8 filter | No |
| EU / US residency | Enterprise in-region | No |
:nitro / :floor model suffixes |
Throughput vs cheapest | No aliases |
| BYOK | User’s OpenAI/Anthropic/… keys, OR credits as fallback | Keys in env / virtual keys; no marketplace mix |
| Plugins (web search, etc.) | Third-party tools on the route | MCP egress exists; no search plugin |
usage split |
prompt / completion / reasoning / cached + cost + upstream cost | prompt/completion; cache_hit bool; no reasoning/cached/cost |
| Native tokenizer | Per-model counts | Provider-reported or chars/4 |
Public /models + rankings |
Live catalog | Local + configured models |
| Guardrails as routing policy | ZDR per key / org member | Guardrails exist; not bound to provider pick |
Measured market facts we must treat as constraints, not goals:
- 400+ models, 80+ providers, 10M+ developers, >10T tokens/day (Stripe / OpenRouter, Aug 2026).
- Same model, 10× price spread across providers (Llama 3.3 70B: $0.10 vs $1.04 / M input, Jun 2026).
- Agents ≈ 5× human tokens; 14× agent volume since Feb 2026; >85% of that is cached-prompt reread.
- Snowflake Cortex, Cloudflare AI Gateway, Bedrock Intelligent Prompt Routing, Azure Foundry all added dynamic model routing in the same window. Routing is table stakes. Local $0 + trusted cache is not.
3. What we can do that OpenRouter cannot¶
These are already shipped. They are not marketed as the answer to the Stripe deal.
| Capability | Evidence |
|---|---|
| Exact L0 replay at 320 rps / 61 ms p95 / 100% hit | docs/developer/resources/benchmark-load.md (M4 Pro) |
| Generate-heavy still $0 on device | same page: 5.0 rps, max_tokens=8, no cache |
| vs LiteLLM on the same Ollama: ~27× on $0 tiers | benchmark-vs-litellm.md |
| vs raw Ollama: 43× median on cache hits | benchmark-comparison.md |
| Cache trust (near-miss reject 100%, paraphrase retention published) | live product bench |
| Lt / CCS — git, lint, pytest without a model | router |
| IDE one-click (Cursor, Claude Code, JetBrains facade, VS Code) | setup recipes |
| Code never leaves the machine on L0–L5 | default X-Daari-No-Frontier path |
OpenRouter cannot offer a 320 rps L0 hit on the developer’s laptop. They can only discount the re-read at the provider. We should make the re-read free and local.
4. Why we have been slow¶
Not a talent problem. A queue problem.
- v2 declared OpenRouter “nothing structural” and parked marketplace work. Correct as a non-goal; wrong as “ignore their API and their agent-token data.”
- auto-dev picks oldest P3 (OIDC
kty,validate_assignment) after P1/P2 emptied. Those fixes are real. They do not move a Cursor user off OpenRouter. - ADR-0004 made the highest-volume client path (tools on every turn) uncacheable. That was right for “don’t serve a stale tool result.” It is wrong as a blanket skip of L0/L1 on the prompt prefix.
- We measure Ask-shaped benches (short chat, no tools). The market is agents. Our published numbers do not show the 5× token problem.
- L6 through OpenRouter is a
base_url, not a product: we do not passprovider, ZDR, sort, or cost.
Until the backlog’s top card is “agent tokens stay on the machine,” we will look like a slower LiteLLM.
5. Trains (build these, in this order)¶
G1 — Agent prefix cache (the Stripe-deal answer)¶
Outcome: A Cursor agent turn that only grew the last tool result hits L0/L1 on the stable prefix (system + tools + history hash), instead of skipping cache entirely.
- Split ADR-0004: still do not cache final tool-call transcripts as if they were Ask answers; do cache prefix embeddings / exact prefix keys.
- Honor provider prompt-cache headers on L6 (Anthropic
cache_control, OpenAI automatic prefix). - Publish an agent mix on the load harness: N tool-bearing turns, report cached-input tokens avoided vs a raw OpenRouter path (priced, not billed).
- Default-suite tests: tool-bearing request with identical prefix → L0 or L1 hit; changed last tool result → miss on suffix only.
Done when: live agent mix shows ≥50% of prompt tokens served at L0/L1 or as local prefix, and the published page says so.
Shipped MVP (#223): exact L0 on identical tools + full history; L1 stays off so a changed tool result cannot cosine-match a prior answer. Agent mix lives on the load page. Prefix-embedding L1 is a later increment.
G2 — OpenRouter-shaped L6 provider object¶
Outcome: The same JSON Cursor already sends to OpenRouter (provider.order, sort, zdr, data_collection, max_price, allow_fallbacks) is honored when daari escalates to L6.
- Parse
provideron OpenAI + Anthropic adapters; store onInternalRequest. - Map onto
frontier.providersslots + OpenRouter passthrough when the slotbase_urlis OpenRouter. - Fail closed: if
zdr: trueand no slot declares ZDR, 4xx — do not silently drop the constraint. - Surface
usage.cost, cached tokens, and chosen provider indaari_meta.
Done when: a fixture request with provider: { zdr: true, sort: "price" } is visible in traces and rejected or routed correctly; OpenRouter integration test (mocked HTTP) covers passthrough.
Shipped MVP (#224): parse on OpenAI + Anthropic; 400 when zdr: true and no slot declares ZDR; OpenRouter slot receives the object; daari_meta carries provider_prefs, cost_usd, cached_tokens.
G3 — First-class OpenRouter backend (use them, don’t clone them)¶
Outcome: frontier.providers[].id: openrouter is documented, tested, and the default L6 hop — BYOK keys still win when set.
- Official slot template +
openrouter/autoand explicit model slugs. - Forward
HTTP-Referer/ title so app attribution works. - Record upstream cost vs daari cost (local = $0) on every L6 row.
:floor/:nitroon the local side as aliases: floor = smallest capable local tier; nitro = warmest / lowest-latency local backend (we already have warm-model preference).
Done when: docs + mocked test + one live optional test (OPENROUTER_API_KEY) that is skipped in CI.
Shipped MVP (#225): openrouter_slot() template, Referer/title headers, daari_cost_usd=0 vs upstream cost_usd, local :floor / :nitro aliases.
G4 — Cost-of-pass routing (stop sending every task to the biggest model)¶
Outcome: Published eval: expected $ and ms to a correct answer by task class, not just “we routed to L3.”
- Extend the live bench with a cost-of-pass column (retry until match or cap).
- Wire latency budget + capability catalog into the default Ask/Agent split so brand-safety / classify / “say hi” never touch L6.
- Dashboard: % of agent prompt tokens that never left the device.
Done when: benchmarks.md has a cost-of-pass table and TRACKING records the $0-tier rate on an agent corpus, not only GP-01–20.
Shipped MVP (#243): live bench publishes a cost-of-pass table (retry until match or cap) and an agent-corpus $0-tier rate from evals/routing/agent.jsonl (tool-bearing Cursor-shaped posts). Latency budget + capability catalog already sit on _choose_initial_tier for Ask and Agent.
G5 — Availability the IDE actually feels¶
Outcome: When Ollama is cold, L4 is down, or L6 429s, the client still gets a well-formed stream — and we say which fallback fired.
- Failover reasons OpenRouter lists: outage, rate limit, context-length, moderation. We retry HTTP; we do not yet remap “context too long” → compress / drop to a long-context slot.
/v1/modelslists local + L6 with capability tags (tools, json, vision, zdr).- Watchdog: replace the brittle
twentylive assertion (#213/#217/#220) so main E2E stops filing noise.
Done when: a context-length error on L3 escalates to L4/L5 or compresses, traced; the three watchdog issues are closed.
Shipped MVP (#244): L3 context-length errors remap to L4/L5 (or compress+retry) with a context_length_failover trace step. /v1/models cards carry capability tags including L6 zdr. Live max_tokens binds on usage + finish_reason (no twenty substring).
G6 — v2 leftovers (do not let these jump the queue)¶
Still worth doing, after G1–G3: extension content script (#171, shipped). B2/B3 (#172), multi-window budgets (#174), IdP-minted keys (#176), synonym verifier (#208), and stalled-auto-merge watch (#200) shipped. These are product, not the OpenRouter gap.
S — Setup / self-service (one-click in every environment)¶
Outcome: The fastest honest path is one command in every supported environment. Docker Compose already is. pip/brew must not require a git clone or a manual ollama pull list.
| Slice | Issue | Status |
|---|---|---|
S1 daari onboard --yes (pip/brew, no clone) |
#256 | shipped |
S2 CI-gate install.sh + mocked doctor |
#257 | shipped |
S3 Default pull = L3 + embed (--minimal skips) |
#258 | shipped |
S4 Cursor --tunnel --yes --daemonize |
#259 | shipped |
S5 User systemd / launchd (daari service) |
#260 | shipped |
| S6 Windows/WSL first-run (honest docs) | #261 | shipped |
S7 daari onboard --serve |
#268 | in this PR |
F1 said pip install daari to a working router in under 5 minutes. Docker hits that. pip did not: daari install looked for scripts/install.sh and died. S1 is the pip/brew hole.
Done when: a pip-only machine with Ollama running can daari onboard --yes && daari serve without cloning the repo, and TRACKING’s install-doctor row is CI-gated (S2).
6. Non-goals (still)¶
- Hosted SaaS marketplace, prepaid credit ledger, public model rankings.
- Training foundation models.
- Matching 400-model SKU coverage on our GPU.
- Replacing Stripe billing.
7. Ninety-day sequence¶
| Window | Train | Why this first |
|---|---|---|
| Days 1–21 | G1 agent prefix cache + agent bench page | This is the 85% token pile |
| Days 22–45 | G2 + G3 provider object + OpenRouter slot | Drop-in for teams that already have an OR key |
| Days 46–70 | G4 cost-of-pass + agent $0-tier in TRACKING | The number we put on the homepage |
| Days 71–90 | G5 failover + models list + watchdog hygiene | Feels like a platform |
auto-dev rule change: P1 cards on this file outrank oldest P3. If the queue is empty of G1–G5, run S (setup/self-service), then P3.
8. Success metrics (not vanity)¶
- Agent-mix $0-tier rate ≥ 70% of prompt tokens on the published corpus.
- Agent-mix implied frontier spend avoided published next to the LiteLLM page.
- L6 OpenRouter passthrough:
zdr/sort/max_pricehonored or hard-fail. - Zero new “twenty” watchdog issues; live
max_tokenstest binds onusage+finish_reason. - Homepage / compare page leads with OpenRouter, not only LiteLLM.
Related¶
- ROADMAP-v2.md — F1–F6 shipped / leftover
- compare.md
- 04-competitive-landscape.md
- ADR-0004 (Ask vs Agent) — amend under G1, do not delete