# Furoshiki — Model Reference

Which models Furoshiki uses per component. Source: `scripts/llm.py`, `memory/design_architecture_reality.md` (via `furoshiki design snapshot`), and the scripts that call them.

## LLM tiers (`scripts/llm.py`)

By default, `call_llm` / `call_llm_chat` use **OpenRouter** (`OPENROUTER_API_KEY`, `https://openrouter.ai/api/v1`). **App attribution:** every OpenRouter `POST /v1/chat/completions` from `scripts/llm.py` includes [App Attribution](https://openrouter.ai/docs/app-attribution) headers so usage can roll up under a public app entry: **`Referer`** (defaults to `https://furoshiki-ai.web.app`), **`X-OpenRouter-Title`** (default `Furoshiki`), and **`X-OpenRouter-Categories`** (default `personal-agent,general-chat`). Toggle with **`FUROSHIKI_OPENROUTER_APP_ATTRIBUTION`** (`0`/`false`/`off` omits these headers). Override URL/title/categories with **`FUROSHIKI_OPENROUTER_APP_REFERER`**, **`FUROSHIKI_OPENROUTER_APP_TITLE`**, **`FUROSHIKI_OPENROUTER_APP_CATEGORIES`** (set categories to **`none`** / **`off`** to omit the categories header).
You can register additional **OpenAI-compatible** providers (Ollama at `http://127.0.0.1:11434/v1`, vLLM, etc.) in **`$FUROSHIKI_DATA/memory/llm_providers.json`** or the dashboard **Models → LLM providers**. Each `MODEL_*` tier in **`llm-tier-models.json`** may be either a plain OpenRouter shortcut / id or **`provider_id::model_id`** (for example `ollama::gemma3:latest`). OpenRouter-only features (`:online` web search, `:exacto`, `reasoning`, response-healing plugin, host `provider` routing, observability `session_id` / `trace`) apply only when the resolved tier targets OpenRouter; other providers receive a minimal OpenAI chat-completions payload. **Estimated spend** for non-OpenRouter models is logged as **$0** (unknown local cost).

**Ollama + Gemma 4:** some builds return an empty `choices[].message.content` on `/v1/chat/completions` while putting text in **`message.reasoning`** (or **`thinking`**). `scripts/llm.py` falls back when `content` is blank: it collapses very long blobs (prefers a lone **`TIER_PROBE_OK`** line for tier probes, else the **last non-empty line** when the trace is long) so normal chat is not flooded with full chain-of-thought. **`cli_tier_probe_*`** uses **`max_tokens` 512** so short probes are not truncated mid-trace at 96 tokens.

Defaults (override with env vars or tier file):


| Tier        | Env var                   | Default (OpenRouter id)             |
| ----------- | ------------------------- | ----------------------------------- |
| **FAST**    | `FUROSHIKI_MODEL_FAST`    | `meta-llama/llama-3.3-70b-instruct` |
| **MICRO**   | `FUROSHIKI_MODEL_MICRO`   | `openai/gpt-oss-20b`                |
| **DEEP**    | `FUROSHIKI_MODEL_DEEP`    | `anthropic/claude-haiku-4.5`        |
| **REFLECT** | `FUROSHIKI_MODEL_REFLECT` | `anthropic/claude-sonnet-4.6`       |
| **PREMIUM** | `FUROSHIKI_MODEL_PREMIUM` | `anthropic/claude-opus-4-6`         |

### Model presets (experiment shortcuts)

**`llm-model-presets.json`** (default under **`$FUROSHIKI_DATA`**, override with **`FUROSHIKI_LLM_MODEL_PRESETS`**) defines extra **shortcut names** → literal **`org/model`** or **`provider::model`** refs. They merge with built-in `MODEL_SHORTCUTS` in **`scripts/llm.py`** for routing, tier files, Telegram **`/model`**, and **`furoshiki config models`**. **Built-in shortcut names always win** if both define the same key. Optional **`input_usd_per_million`** / **`output_usd_per_million`** per preset improve **estimated** spend when the provider omits **`usage.cost`**. Edit via dashboard **Models → Model presets**, **`GET`/`PUT /dashboard/api/llm-model-presets`**, **`GET`/`PUT /api/v1/llm-model-presets`**, or **`furoshiki llm-model-presets show|validate|write`**. You can still type any full model id in **LLM routing** or **Model tiers** without creating a preset.

### Verify tier routing (local vs OpenRouter)

Resolution order is **`llm-tier-models.json` (when that tier key is set)** → **env (`FUROSHIKI_MODEL_*`)** → **code default**. If a tier appears in the JSON file, **`.env` does not override it**; remove or clear the file entry (or the key) if you need env-only control for that tier.

- **`furoshiki models effective`** (`--json`) — merged resolution for every `MODEL_*` tier: whether the value came from **`llm-tier-models.json`**, **env**, or code default, plus **provider id** and **model id** (for example `ollama` + `gemma3:4b` when the tier is `ollama::gemma3:4b`).
- **`furoshiki models probe fast`** (`--json`) — one minimal **`call_llm`** using routing source **`cli_tier_probe_fast`**, which pins **`model: MODEL_FAST`** the same way production FAST sources do. On success, **`llm_call_log`** stores **`source=cli_tier_probe_fast`** and **`model=`** as `provider_id::model_id` for non-OpenRouter providers. Other tiers: **`furoshiki models probe micro|deep|reflect|premium`**.

### LLM spend caps

Enforced in `scripts/llm.py` before each call. If **`memory/budget_settings.json`** exists, its `daily_usd`, `monthly_usd`, and `alert_pct` override **`FUROSHIKI_BUDGET_DAILY_USD`**, **`FUROSHIKI_BUDGET_MONTHLY_USD`**, and **`FUROSHIKI_BUDGET_ALERT_PCT`** for the whole install. Edit via dashboard **Config & tools → LLM budgets**, **`PUT /api/v1/budget`**, or **`furoshiki config set budget-daily|budget-monthly|budget-alert`**. Remove the file (dashboard **Remove JSON**, **`DELETE /api/v1/budget`**, or delete on disk) to fall back to `.env` / code defaults.

**Tracked vs estimated:** each successful completion still stores **`estimated_cost_usd`** from `llm.py`’s internal price table. When OpenRouter returns **`usage.cost`** in the JSON body, it is persisted as **`provider_cost_usd`** (nullable). Budget checks and dashboard **Cost analytics** use **`COALESCE(provider_cost_usd, estimated_cost_usd)`** per row so caps and summaries follow provider-reported spend when available. Run **`furoshiki db migrate`** to add the column on existing databases.

**Rolling averages:** `GET /api/v1/budget` includes **`spend.analytics.rolling`** with **`last_7_days`** and **`last_30_days`**: each has **`avg_daily_tracked_usd`** (total tracked in the UTC window ÷ N), **`avg_daily_openrouter_usd`** (sum of **`provider_cost_usd`** only ÷ N), and matching totals. N is calendar days including today.

**Provider routing:** every OpenRouter request includes `provider.sort` from `llm-routing.json` **`openrouter_provider_sort`** (default `latency`; alternatives `throughput`, `price`). See [OpenRouter provider selection](https://openrouter.ai/docs/guides/routing/provider-selection).

**Broadcast / observability:** when an observability scope is active, requests also include OpenRouter **`session_id`** and a **`trace`** object (`trace_id`, `trace_name`, `generation_name` = the call’s **`source=`**, plus **`tool_loop_round`** in tool loops). Values mirror columns **`session_id`** and **`trace_id`** on **`llm_call_log`** (after **`furoshiki db migrate`**). Telegram sets scope per inbound turn: **`session_id`** is `telegram:<chat_id>:<thread_uuid>` where **`thread_uuid`** stays stable until **`FUROSHIKI_TELEGRAM_OPENROUTER_SESSION_GAP_MIN`** minutes (default **30**, same idea as the listener’s new-session gap) pass without a user message in that process; cron scripts use **`cron_observability_scope`** in `llm.py`. Subprocess plugins inherit **`FUROSHIKI_LLM_SESSION_ID`**, **`FUROSHIKI_LLM_TRACE_ID`**, **`FUROSHIKI_LLM_TRACE_NAME`**. See [OpenRouter Broadcast](https://openrouter.ai/docs/guides/features/broadcast/overview).

**Scheduler vs routing (cost attribution):** each LLM call logs `source=` (a routing profile key such as `post_conversation` or `voice_dispatcher`). The **Brain** reads **`$FUROSHIKI_DATA/memory/schedules.json`** (not OS cron). To see which scheduled tasks map to which routing sources and resolved models, use the dashboard **Models → Scheduler & models** (API: **`GET /dashboard/api/schedule-llm-routing`**, admin key) or run **`furoshiki llm-routing schedule`** (`--json`; optional `--schedules PATH`). If `schedules.json` is missing or empty, the report falls back to parsing the repo **`cron/crontab`** as a reference only. Use **`--legacy-crontab-only`** to force that file. The report also lists scripts that are not scheduler tasks but still define routing rows (e.g. Telegram listener). Indirect chains (e.g. `outreach_pulse.py` → `voice_dispatcher`) are annotated in `scripts/llm_routing_config.py` (`SCHEDULER_INDIRECT_ROUTING_SOURCES`).

**``default`` vs named ``sources``:** merged `llm-routing.json` supplies a **`default`** profile for any `source=` string that does not have its own row. If you set **`default.model`** to a **literal** OpenRouter id (e.g. `meta-llama/llama-3.3-70b-instruct`), that id **overrides** the Python `model=` argument for every caller that still resolves through `default` — including paths that intended **`MODEL_FAST`** from `llm-tier-models.json` / Ollama. Prefer leaving **`default.model`** as **`MODEL_FAST`** (or omitting `model` there), and use **named source keys** for per-path tuning. Code defaults in `scripts/llm.py` define a row for each production `source=` (e.g. `user_fact_jit`, `curiosity_explore`, `reply_defer_ack`) so they no longer depend on `default` for tier choice.

**Routing rows with no default `model`:** `behavioral_commitment_eval` and `analytics_report` omit **`model`** in code defaults so the **call site** (or report request) supplies the tier / id. In the dashboard **LLM routing** table, leave the model dropdown on **— inherit —** for those sources; saving with a forced model would pin a single tier and break the commitment eval’s FAST-vs-REFLECT split unless you intentionally override.

**`:exacto` variant:** per-source `model` strings in `llm-routing.json` (or code defaults) may use the suffix **`:exacto`** on a tier or full id (e.g. `MODEL_FAST:exacto`, `MODEL_MICRO:exacto`). That requests OpenRouter [quality-first provider sorting for tool use](https://openrouter.ai/docs/guides/routing/model-variants/exacto). Furoshiki applies it to JSON/tool-heavy sources: `micro_brain_inbound`, `micro_brain_delegation_brief`, `classify_intent`, `skill_orchestrator_plan`.

**Response healing:** when `call_llm` / `call_llm_chat` is invoked with **`response_format`** (e.g. `{"type": "json_object"}`), the request includes OpenRouter’s **`response-healing`** plugin so malformed JSON can be repaired. Used by `classify_intent`, `classify_tasks_nl`, and structured JSON paths in `micro_brain.py`.

**Context compression:** set **`FUROSHIKI_OPENROUTER_CONTEXT_COMPRESSION=1`** to add OpenRouter’s **`context-compression`** plugin on long reflection `source=` values (`inner_monologue`, `post_conversation`, `deep_reflection`, related post-conversation sub-passes, `daily_analysis`). Default **off**. Middle-out truncation when prompts exceed context limits.

**Callable skills (Telegram):** the main chat completion uses **native OpenRouter tool calling** (`tools` + tool loop) when callable plugins are installed, instead of a separate FAST “plan” JSON call — see `call_llm_chat_with_tool_loop` in `scripts/llm.py` and `plan_and_invoke` in `scripts/skill_orchestrator.py`.

**Extended reasoning:** per-source **`thinking_budget_tokens`** (positive integer) in code defaults and/or `llm-routing.json` sets OpenRouter’s unified [`reasoning.max_tokens`](https://openrouter.ai/docs/best-practices/reasoning-tokens) on chat completions (Anthropic tiers by default). Omitted keys inherit from code defaults because `llm-routing.json` is merged **per source** into `llm.py` defaults.

**MICRO** — `scripts/micro_brain.py`: one batched JSON call per Telegram user turn (routing flags), plus small optional calls for reply-queue extract/close and echo detection, proactive **outreach polish** after `voice_dispatcher` drafts a message (`source=micro_brain_outreach`; **`FUROSHIKI_MICRO_BRAIN_OUTREACH`**, default on), and **`synthesize_delegation_task_brief`** (`source=micro_brain_delegation_brief`) on dispatch with a generous **`FUROSHIKI_DELEGATION_BRIEF_MAX_TOKENS`** (default **2048**) so the worker task string is not truncated. Short `max_tokens` on inbound routing only; brief synthesis uses the higher cap. Costs stay small vs. the listener chat model. Separately, **`telegram_preamble_status`** (`scripts/telegram_preamble_status.py`) may emit one short “still working” Telegram line while the main listener reply is generating — gated by delay, probability, cooldown, and micro/template mix. Effective tuning: **`memory/listener_advanced_settings.json`** (Dashboard **Advanced**) when present, else **`FUROSHIKI_TELEGRAM_PREAMBLE_*`** in `.env` (`scripts/listener_advanced_settings.py`).

## OpenRouter web search (`:online`)

Furoshiki can request [OpenRouter web search](https://openrouter.ai/docs/guides/features/plugins/web-search) by appending `:online` to the model id (equivalent to the `web` plugin). That adds **extra cost** on top of token usage (native provider search or Exa-backed fallback per OpenRouter’s rules).

| Policy | Behavior |
|--------|----------|
| **Reflection scripts** | Web is **on** for named `source=` values in `REFLECTION_WEB_SOURCES` in `scripts/llm.py` (inner monologue, morning/afternoon processing, deep reflection passes, post-conversation and related sub-passes). Infrequent runs; uses current public context without a separate browser integration. |
| **Telegram chat** | Web is **off** unless routing says the user needs live public facts. With **`FUROSHIKI_MICRO_BRAIN_INBOUND=1`** (default), **`needs_web`** is one field in a single **MICRO** JSON batch (`source=micro_brain_inbound`); URLs still force on, whole-message chitchat forces off, and keyword regex fills gaps on parse failure. Set **`FUROSHIKI_TELEGRAM_WEB_GATE_LLM=0`** to use **keyword-only** `needs_web` (ignore the model on that field while still running the batch for other flags). With **`FUROSHIKI_MICRO_BRAIN_INBOUND=0`**, legacy path: optional **FAST** `telegram_web_gate` call when `FUROSHIKI_TELEGRAM_WEB_GATE_LLM=1`, else keyword-only. |
| **Plugins / callable skills** | If `permissions.openrouter_web: true` in `PLUGIN.md`, the plugin subprocess gets `FUROSHIKI_PLUGIN_OPENROUTER_WEB=1` and `call_llm` from that process may use `:online`. Toggle from the dashboard **Extensions → Installed plugins** (Web column) or edit `PLUGIN.md`. |
| **Kill switch** | `FUROSHIKI_OPENROUTER_WEB=0` disables **all** automatic `:online` (reflection, plugin env, and explicit telegram overrides). |

**Env vars (optional):**

- `FUROSHIKI_OPENROUTER_WEB` — default on; set `0` to disable web everywhere above.
- `FUROSHIKI_TELEGRAM_OPENROUTER_WEB` — default on; set `0` to never use web on Telegram even when the gate passes.
- `FUROSHIKI_TELEGRAM_WEB_GATE_LLM` — when micro-brain inbound is on: default `1` uses the model for `needs_web`; set `0` for keyword-only `needs_web`. When inbound is off: same as before (FAST `telegram_web_gate` vs keyword-only).
- `FUROSHIKI_MODEL_MICRO` — default `openai/gpt-oss-20b`.
- `FUROSHIKI_MICRO_BRAIN_INBOUND` — default `1`; batched routing JSON per Telegram message.
- `FUROSHIKI_MICRO_BRAIN_ECHO` — default `1`; LLM echo check with SequenceMatcher fallback.
- `FUROSHIKI_MICRO_BRAIN_REPLY_QUEUE` — default `1`; substantive question extract + selective close of pending nudges.
- `FUROSHIKI_MICRO_BRAIN_OUTREACH` — default `1`; `MODEL_MICRO` JSON polish on `voice_dispatcher` proactive drafts before Telegram send.
- `FUROSHIKI_DELEGATION_CONTEXT_TURNS` — default `20`; max transcript lines for delegation classify + brief.
- `FUROSHIKI_DELEGATION_CONTEXT_MAX_CHARS` — default `10000`; hard cap on transcript text.
- `FUROSHIKI_DELEGATION_THREAD_BRIEF` — default `1`; set `0` to skip MICRO task-brief synthesis (use FAST `task_description` only).
- `FUROSHIKI_DELEGATION_BRIEF_MAX_TOKENS` — default `2048`; output budget for `task_brief` JSON.
- `FUROSHIKI_DELEGATION_LINE_MAX_CHARS` — default `2400`; per-bubble cap inside the transcript formatter.

OpenRouter may return URL citations in `annotations` on web-enabled completions; **`telegram_listener`** appends a short **Sources:** footer to the reply when annotations include URLs.

**Deprecated:** Separate “web-search” plugins that called Brave or an external browser to paste context into reflections are **obsolete** — use OpenRouter web on reflection tiers and plugin `openrouter_web` instead.


### Telegram media (dashboard: Models → LLM routing → Telegram — media)

| Source | Default model | Role |
|--------|---------------|------|
| `media_image_analysis` | `google/gemini-3-flash-preview` (`gemini3flash`) | Vision analysis for Telegram photos |
| `media_audio_transcription` | `openai/whisper-1` (`whisper`) | Voice/audio STT (detached worker) |

Change models in **Models → LLM routing** (persisted to `memory/llm-routing.json`). Byte caps and ack text: Dashboard **Settings → Operator → Media**.

## Where each tier is used

- **FAST** — `classify_intent.py` (delegation / Q–A routing), `run_micro_contemplation.py`. `telegram_listener.py` uses `FUROSHIKI_LISTENER_MODEL` if set, otherwise **DEEP**; `/model` can switch to any shortcut from built-in `MODEL_SHORTCUTS` plus **`llm-model-presets.json`** (see above).
- **MICRO** — `micro_brain.py` (Telegram inbound batch, reply-queue helpers, echo check, voice outreach polish, delegation task-brief synthesis on queue).
- **DEEP** — `telegram_listener.py` (default), `voice_dispatcher.py`, `self_questions_roundup.py` (consolidated “questions for you” Telegram path), `run_emotional_read.py`, `run_afternoon_processing.py`, `run_morning_reflection.py`, `run_question_processing.py`, `run_self_diagnosis.py`, `run_repair_digest.py` (repair digest cron; `source=repair_digest`, light extended reasoning), `run_deep_reflection.py` (audit pass only), `agent_runner.py` (source `agent_runner`; each active agent task tick).
- **REFLECT** — `run_inner_monologue.py`, `run_post_conversation.py`.
- **PREMIUM** — `run_deep_reflection.py` (main reflection pass only — the weekly SELF.md rewrite).

## Not OpenRouter

- **Delegation, repair, self-modification** (`delegation_dispatcher.py`, `repair_dispatcher.py`, `self_modification_dispatcher.py`) — `claude` CLI at `~/.local/bin/claude` (OAuth / local Claude Code config), not the tier constants in `llm.py`.
- **Embeddings** — ChromaDB's local `all-MiniLM-L6-v2` (no API).

## OpenRouter shortcut aliases (`MODEL_SHORTCUTS`)

Besides tier defaults, `scripts/llm.py` defines optional shortcuts for **`/model`**, **`furoshiki models set …`**, and **`furoshiki config models`**. Operator-defined aliases in **`llm-model-presets.json`** use the same merge rules (built-in names win on collision). Recent built-in additions:

| Shortcut | Resolves to |
|----------|-------------|
| `deepseek32` | `deepseek/deepseek-v3.2` |
| `gemini3flash` | `google/gemini-3-flash-preview` |
| `mistral2603` | `mistralai/mistral-small-2603` |

`_COST_TABLE` uses OpenRouter’s published list prices where available; **`usage.cost`** from the API overrides in logs when present.

## Managing models via CLI

```bash
furoshiki models                        # show all component → tier → current model
furoshiki models set deep sonnet        # switch DEEP tier to Sonnet (shortcut expanded)
furoshiki models set premium haiku      # experiment: use Haiku for deep reflection
furoshiki models reset deep             # restore DEEP tier to code default (Haiku)
furoshiki models reset                  # reset all tiers to defaults
furoshiki config models                 # show all shortcuts with cost/1M table
furoshiki llm-model-presets show       # experiment aliases JSON path + presets + built-in keys
```

Tier names for `set`/`reset`: `fast`, `deep`, `reflect`, `premium`, `listener`.

## Logging caveat

Some `cron_run_log` rows may record an old model in `model_used` if they pre-date this refactor. Trust the `call_llm(..., model=...)` argument in the script for the real inference model.

---

## Model changes (applied 2026-03-27)

Reviewed 2026-03-26. Principle: match model capability to cognitive demand.

### Applied upgrades

| Script | Was | Now | Rationale |
|--------|-----|-----|-----------|
| `run_emotional_read.py` | REASON (gpt-oss-20b) | **DEEP** (Haiku) | Detecting emotional subtext is nuanced; wrong reads cascade. |
| `run_afternoon_processing.py` | REASON (gpt-oss-20b) | **DEEP** (Haiku) | Structured reflection + JSON; gpt-oss-20b produced flat output. |
| `run_deep_reflection.py` (main) | REFLECT (Sonnet) | **PREMIUM** (Opus) | Highest-stakes call — rewrites SELF.md, personality drift. Runs once/week (~$0.20/run). |
| `voice_dispatcher.py` | Groq llama-3.3-70b-versatile | **DEEP** (Haiku) via OpenRouter | Migrated off separate Groq path. Now budget-tracked and tier-managed. |
| `telegram_listener.py` default | FAST (llama-3.3-70b) | **DEEP** (Haiku) | Primary interface — warmer, more contextually aware replies. `/model` still available. |

### Applied downgrades

| Script | Was | Now | Rationale |
|--------|-----|-----|-----------|
| `run_morning_reflection.py` | REFLECT (Sonnet) | **DEEP** (Haiku) | 2-4 sentence scratch note. Sonnet's depth was wasted. |
| `run_question_processing.py` | REFLECT (Sonnet) | **DEEP** (Haiku) | Structured Q/A: pattern matching against evidence, not creative synthesis. |
| `run_self_questions_contemplation.py` | — | **FAST** | Batch JSON: dedupe, dismiss, resolve from recent chat excerpt. |
| `self_questions_roundup.py` | — | **DEEP** (Haiku) | One Telegram message consolidating top `ask_user` pending rows (`source=self_questions_roundup`). |
| `run_self_diagnosis.py` | REFLECT (Sonnet) | **DEEP** (Haiku) | Technical log analysis and health scoring. Analytical, not introspective. |
| `run_micro_contemplation.py` | REASON (gpt-oss-20b) | **FAST** (llama-3.3-70b) | Runs frequently, short output. Llama-70b writes more natural short prose. |

### Kept as-is

| Script | Tier | Why |
|--------|------|-----|
| `classify_intent.py` | FAST | Pure classification, structured JSON, short prompts. Speed matters. |
| `run_inner_monologue.py` | REFLECT | Nightly journal — core identity artifact. Needs narrative voice and full-day synthesis. |
| `run_post_conversation.py` | REFLECT | Extracts user facts / profile signals, quality scoring, contradiction detection. Touches core memory. |
| `run_deep_reflection.py` (audit) | DEEP | Lighter model reviewing heavier model's work. Appropriate asymmetry. |
| Delegation / repair / self-mod | Claude CLI | Agentic coding with tool use. CLI is the right tool. |
| Embeddings | all-MiniLM-L6-v2 | Local, free, adequate for search patterns. |

### Net cost impact

Downgrades (morning reflection, question processing, self-diagnosis: Sonnet → Haiku) save more than the modest increases (emotional read, afternoon processing: gpt-oss-20b → Haiku). Listener default to Haiku is the biggest cost increase but also the biggest UX win. Deep reflection to Opus once/week is negligible (~$0.20/week).

---

## Related

- Shortcuts and costs: `scripts/llm.py` (`MODEL_SHORTCUTS`, `_COST_TABLE`)
- CLI: `furoshiki models`, `furoshiki config models`
- Telegram: `/model`
