Skip to content
Back to skills

Iblai Vibe Local Llm

DSecurity

Use when adding on-device LLM inference (Ollama backend) to a vibe Next.js + Tauri app. Documents the contract a vibe app must implement so the SDK can route chat through a local model via `localLLMProps` — the Tauri command names, event names, and the React hook shape. Mention before wiring "offline AI", "local model", "on-device LLM", or "Ollama" support into a desktop build.

  • 15 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 2, 2026
developmenttypescriptrustgoshellreactnextjsnodegitapifrontend

Works with

  • cli
  • api

Security analysis

D42/100
  • criticalPipes output to a shell interpreter
  • criticalDownloads and executes remote scripts — classic supply chain attack
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 8 files and shows the line behind each finding

Scanned September 20, 2026

npx -y skills add iblai/vibe --skill iblai-vibe-local-llm --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Iblai Vibe Local Llm?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Iblai Vibe Local Llm
[![Security: D — Skills Directory](https://www.skillsdirectory.com/api/skills/iblai-iblai-vibe-local-llm/badge)](https://www.skillsdirectory.com/skills/iblai-iblai-vibe-local-llm)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: iblai-vibe-local-llm
description: Use when adding on-device LLM inference (Ollama backend) to a vibe Next.js + Tauri app. Documents the contract a vibe app must implement so the SDK can route chat through a local model via `localLLMProps` — the Tauri command names, event names, and the React hook shape. Mention before wiring "offline AI", "local model", "on-device LLM", or "Ollama" support into a desktop build.
globs:
alwaysApply: false
metadata:
  kind: guide
---

# /iblai-vibe-local-llm

A vibe Next.js + Tauri app can serve chat from a model running on the
user's machine instead of the hosted backend. The SDK turns this on
automatically when you pass a `localLLMProps` object whose
`isAvailable` is `true` and whose `ollamaStatus.model_installed` (or
`foundryStatus.has_models`) reports a ready model.

This skill defines the **contract** your app must implement to feed
that prop:

- the Tauri `#[command]` names the React layer calls,
- the Tauri event names the React layer listens on,
- the shape the React hook must return,

and points to references describing the install-flow, the on-disk
layout, and the failure modes you'll hit.

It does **not** ship a reference implementation — local-LLM support
is closed-source in the apps that have it today.

> Do NOT add custom styles / colors to ibl.ai SDK components. They
> ship with their own styling.
> See [BRAND.md](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/BRAND.md)
> for the brand palette.

> **Common setup (env, conventions, verification):** see
> [docs/skill-setup.md](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/docs/skill-setup.md).

## Prerequisites

1. Tauri shell wired up via [`/iblai-vibe-ops-build`](../../ship/iblai-vibe-ops-build/SKILL.md)
   (adds `src-tauri/` + `pnpm install`). The skill assumes
   `src-tauri/` already exists. **Heads-up:** on an app that already has
   a non-trivial `next.config.mjs`, adding the Tauri shell can mis-merge its
   `output: 'export'` / `images` / `allowedDevOrigins` injection (it has
   landed those *inside the wrong object*, e.g. an SSR polyfill, leaving
   them as dead code). `git diff next.config.mjs` right after running it.
2. Rust toolchain via [rustup](https://rustup.rs), **plus the Tauri
   system libraries** for your OS (Linux: `libwebkit2gtk-4.1-dev`,
   `libdbus-1-dev`, `libsoup-3.0-dev`, …). These build *before* your
   crate, so a missing one (e.g. `libdbus-sys` panicking) masks your own
   compile errors entirely.
3. ibl.ai providers in place (`/iblai-vibe-auth`) — the SDK looks for
   `localLLMProps` on its account/profile **dropdown** surfaces
   (`UserProfileDropdown` / `UserProfileModal`), not on `Account`/`Chat`.

## The contract

### 1 — Tauri command + event names

Drop [`assets/nextjs/types/tauri.ts`](assets/nextjs/types/tauri.ts) into
your project at `types/tauri.ts`. It exports:

- `TAURI_COMMANDS` — every `#[tauri::command]` name your Rust side
  must register (`install_ollama`, `get_system_memory`, `download_model`,
  `ollama_chat_stream`, …)
- `TAURI_EVENTS` — every `app.emit(...)` name your Rust side must
  use (`model:download-progress`, `model:installation-log`, …)
- `OllamaStatus`, `DownloadProgress`, `InstallationLog`,
  `DiskSpaceError`, `FoundryStatus` — the payload types each
  command / event must conform to
- `isTauriApp()` — synchronous check for `__TAURI_INTERNALS__` on
  `window`

These are the only assets the skill ships — they're public-API
constants and TypeScript interfaces, not implementation.

### 2 — Rust commands to implement

Your `src-tauri/src/` needs `#[tauri::command]` async functions
matching the names in `TAURI_COMMANDS` and the signatures in
[`references/tauri-commands.md`](references/tauri-commands.md). At
minimum (Ollama path):

```
install_ollama                  -> Result<String, String>
stop_ollama                     -> Result<(), String>        (onStopManager / toggle off)
check_ollama_status             -> Result<OllamaStatus, String>
check_disk_space_for_model      -> Result<bool, String>      (emits disk-space-error)
get_system_memory               -> SystemMemory              (sync; RAM via sysinfo, VRAM via nvidia-smi)
download_model(model_id)        -> Result<(), String>        (emits download-progress)
cancel_model_download           -> Result<(), String>
check_network_status            -> Result<bool, String>
get_os_type                     -> String
ollama_chat                     -> Result<String, String>
ollama_chat_stream              -> Result<(), String>        (emits ollama:chunk/done/error)
```

`download_model` takes the Ollama tag as an argument (the SDK's Local-LLM
tab owns the catalog and passes the chosen id) — it is **not** the old
hardcoded `download_phi3_model`. `get_system_memory` is a synchronous
probe used to warn before pulling a model too big for the host.

Wire them into `.invoke_handler(tauri::generate_handler![...])`. Add
the Cargo deps listed in
[`references/cargo-deps.toml`](references/cargo-deps.toml).

### 3 — React hook shape

`localLLMProps` is read by the SDK's account/profile **dropdown**
surfaces (see "Use it"). **Grep your installed `@iblai/web-containers`
`.d.ts` for `localLLMProps` and match its exact shape** — it is the
ground truth, and it differs from earlier drafts of this skill. As of
`@iblai/web-containers` 1.8.x the shape is:

```ts
{
  isAvailable: boolean              // isTauriApp() && commands registered
  state: ModelDownloadState         // status/progress/message/logs/lastUpdated
  ollamaStatus: OllamaStatus | null
  systemMemory?: SystemMemory | null  // { ram_total, vram_total } from get_system_memory
  isUsingFoundry?: boolean          // Windows NPU path
  foundryStatus?: FoundryStatus | null
  foundryModels?: FoundryModel[]
  selectedFoundryModel?: string | null
  onStartDownload: (modelId?: string) => void  // install (if needed) + pull that tag
  onCancelDownload: () => void
  onInstallOllama: () => void
  onStopManager?: () => void        // stop_ollama — fired when the toggle goes off
  onInstallFoundry?: () => void
  onCheckStatus: () => void
  onResetState: () => void
  onSelectFoundryModel?: (modelId: string) => void
}
```

Note the callbacks are **`on*`-prefixed and return `void`** (they fire
the `invoke` internally — don't return the promise). `onStartDownload`
now takes an optional `modelId` — the tab passes the catalog entry the
user picked, and the SDK forwards it to `download_model`.

**The SDK ships the hook.** `@iblai/web-containers` exports
`useModelDownload()`, which already does the work below and returns
`{ isAvailable, state, ollamaStatus, systemMemory, startDownload(modelId?),
cancelDownload, installOllama, stopManager, checkStatus, resetState, … }`.
Wire its output into `localLLMProps` (renaming to the `on*` callbacks)
rather than re-implementing — that's how the shipped desktop app does it.
If you do hand-roll the hook, it must:
- `invoke(TAURI_COMMANDS.CHECK_OLLAMA_STATUS)` on mount, after an
  opportunistic `CHECK_FOUNDRY_STATUS` that silently falls back when the
  Foundry commands aren't registered (the `invoke` just rejects),
- `invoke(TAURI_COMMANDS.GET_SYSTEM_MEMORY)` once on mount and keep
  `systemMemory` so a Download click can be pre-flighted against host
  capacity (see "Field notes"),
- `listen` on every `model:*` event to drive `state`,
- persist `state` to `localStorage.model_download_state` so progress
  survives reload — **hydrate it in an effect, not the initial
  `useState`**, or you'll get an SSR hydration mismatch.

`onStartDownload(modelId)` calls `install_ollama` (if not installed) then
`download_model(modelId)`; `onCancelDownload` calls
`cancel_model_download`; `onStopManager` calls `stop_ollama`.

## Use it

Pass the hook output to whichever surface accepts `localLLMProps` in
**your installed SDK**. As of `@iblai/web-containers` 1.7.x that is
`UserProfileDropdown` / `UserProfileModal` (the "Local LLM" account tab)
— **not** `Account` or `Chat`. Confirm with
`grep -rn localLLMProps node_modules/@iblai/web-containers/dist/**/*.d.ts`
before wiring; the earlier `<Account localLLMProps>` example was wrong
for shipped SDKs.

```tsx
"use client";
import { useLocalLLM } from "@/hooks/use-local-llm";
import { UserProfileDropdown } from "@iblai/iblai-js/web-containers/next";

export function AccountMenu(props) {
  const localLLMProps = useLocalLLM();
  return <UserProfileDropdown {...props} localLLMProps={localLLMProps} />;
}
```

When `isAvailable` is `false` (a normal browser tab), the props degrade
to no-ops and the SDK uses the hosted backend.

**Chat routing is automatic — `<Chat>` does not take `localLLMProps`.**
Once `isAvailable && (ollamaStatus.model_installed ||
foundryStatus.has_models)`, the SDK invokes the registered
`ollama_chat_stream` command and listens on `ollama:chunk` /
`ollama:done` / `ollama:error` on its own. Your only job for chat is to
register the Tauri commands with the exact names/events from
`types/tauri.ts` — no per-surface wiring.

## Backend choice

| Backend | Recommended for | Default model |
|---|---|---|
| **Ollama** | Cross-platform default — macOS, Linux, Windows x64 | Phi-3 Mini (~2 GB) |
| **Foundry Local** | Windows ARM / Copilot+ PCs with NPU | qwen2.5-0.5b (~1 GB) |

The hook should call `check_foundry_status` first and fall back to
Ollama when it rejects or returns `is_supported: false`. See
[`references/foundry-status.md`](references/foundry-status.md) for
the Foundry-specific command names and lifecycle.

## Field notes (from a production integration)

- **Warn before downloading a model too big for the machine.** Call
  `get_system_memory` → `{ ram_total, vram_total }` (bytes) and size the
  model against `usable = max(ram_total, vram_total)`. `vram_total` comes
  from `nvidia-smi` and is **`0`** on integrated graphics, Apple unified
  memory, and AMD/Intel GPUs — so thresholding on VRAM alone wrongly
  flags every Mac; always fall back to RAM via the `max`. Parse the
  catalog's human size (`"2.2 GB"` → bytes) and, when it exceeds
  `usable * FRACTION`, show a confirm dialog instead of pulling:

  ```ts
  const usableMemoryBytes = (m?: SystemMemory | null) =>
    m ? Math.max(m.ram_total, m.vram_total) : 0;          // VRAM 0 ⇒ use RAM
  const modelExceedsCapacity = (model, m) => {
    const bytes = parseModelSizeBytes(model.size);         // "2.2 GB" → number
    if (bytes == null) return false;                       // unknown size ⇒ don't warn
    return bytes > usableMemoryBytes(m) * MODEL_SIZE_WARN_FRACTION;
  };
  ```

  `MODEL_SIZE_WARN_FRACTION` is the share of usable memory a model may
  occupy before the warning trips (a realistic ceiling is ~0.5–0.8; the
  shipped tab ships a deliberately tiny test value so the dialog is easy
  to trigger — set it sanely before release). **Gotcha:** read memory
  *before* any catalog-lookup / early-return guard, or the `invoke`
  silently never fires — see [troubleshooting](references/troubleshooting.md)
  "A command never runs".

- **Server-rendered apps can't use the default static export.** The
  Tauri shell defaults to `frontendDist: "../out"` (a `next build`
  static export). An app with runtime-dynamic routes (e.g.
  `/x/[id]/[[...slug]]`) and no `generateStaticParams` **cannot** be
  exported — `output: 'export'` hard-errors with "missing
  generateStaticParams()". Keep `output: 'standalone'` and point Tauri
  **dev** at the live Next server instead, in `tauri.conf.json`:
  ```json
  "build": {
    "beforeDevCommand": "pnpm dev",
    "devUrl": "http://localhost:3000",
    "beforeBuildCommand": "pnpm build",
    "frontendDist": "../out"
  }
  ```
  `tauri dev` then loads `localhost:3000`; no `out/` needed. (Production
  bundles for such apps point `frontendDist`/`devUrl` at a hosted URL
  rather than shipping a static export.)

- **Download via the daemon HTTP API, not the CLI.** Implementing
  `download_model(modelId)` as `POST 127.0.0.1:11434/api/pull
  {"model": modelId, "stream":true}` and reading the NDJSON stream
  (`{status,completed,total,digest}`) is cleaner than scraping
  `ollama pull` stdout — it maps straight onto the `DownloadProgress`
  fields and cancels by breaking the stream loop on a shared
  `AtomicBool` (no child-process to kill). Same pattern for chat:
  stream `POST /api/chat`.

- **`install_ollama` needs root on Linux.** The official `install.sh`
  writes `/usr/local/bin/ollama` and creates an `ollama` systemd
  service + user via `sudo`. A non-root Tauri app spawning
  `curl … | sh` non-interactively can't elevate, so the install fails
  silently. Prefer a rootless install (drop the static `ollama` binary
  in `~/.local/bin`, run `ollama serve`) or detect-and-guide instead of
  auto-installing.

- **Minimal Cargo deps** for the HTTP-API approach:
  `reqwest { json, stream, rustls-tls }`, `tokio { full }`,
  `futures-util`, `sysinfo`, `chrono`. `blocking` / `sha2` / `hex` /
  `http` are only needed if you verify installer checksums. `sysinfo`
  also backs `get_system_memory`'s RAM figure (`System::total_memory()`);
  VRAM is read by shelling out to `nvidia-smi`, so it needs **no extra
  crate** — and contributes `0` wherever that binary is absent.

## References

- [`references/architecture.md`](references/architecture.md) — layer
  diagram, lifecycle, on-disk model paths.
- [`references/tauri-commands.md`](references/tauri-commands.md) —
  full signatures + event payloads for every command in the contract.
- [`references/foundry-status.md`](references/foundry-status.md) —
  Windows NPU path command surface.
- [`references/cargo-deps.toml`](references/cargo-deps.toml) — Cargo
  `[dependencies]` your Rust side needs.
- [`references/troubleshooting.md`](references/troubleshooting.md) —
  common install / runtime failures with root causes.

## Related skills

- [`/iblai-vibe-ops-build`](../../ship/iblai-vibe-ops-build/SKILL.md) — adds the Tauri shell this skill depends on.
- [`/iblai-vibe-account`](../../organizations/iblai-vibe-account/SKILL.md) — Account / settings surface that consumes `localLLMProps`.
- [`/iblai-vibe-agent-chat`](../../agents/iblai-vibe-agent-chat/SKILL.md) — chat surface that routes through the local model.
- [`/iblai-vibe-agent-llm`](../../agents/iblai-vibe-agent-llm/SKILL.md) — the per-agent LLM-provider Settings tab.

Files in this skill

  • SKILL.md14 KB
  • assets/nextjs/types/tauri.ts4 KB
  • references/architecture.md5.4 KB
  • references/cargo-deps.toml1.6 KB
  • references/foundry-status.md2.7 KB
  • references/tauri-commands.md6.8 KB
  • references/troubleshooting.md8 KB
  • test.json76 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.