[object Object]

← back to Exo

feat(dashboard): add Pi integration tab (#1925)

4939fbe9952b187dc1adf34efce234f36413b6cc · 2026-04-22 21:29:47 +0400 · Nadeem Hilal Wani

## Summary
Adds a new **Pi** tab to the Integrations page (`/#/integrations`)
alongside the existing Claude Code, OpenCode, Codex, OpenClaw, Open
WebUI, n8n, and Firefox tabs.

[pi](https://pi.dev) (`@mariozechner/pi-coding-agent`) is a terminal
coding agent that supports custom OpenAI-compatible providers via
`~/.pi/agent/models.json`.
This tab gives users a copy-pasteable config to wire pi up to their exo
cluster.

## What's in the tab
- **Model selector** (shown when multiple models are running) — picks
the default model for the generated shell command.
- **Models Config card** — generates `~/.pi/agent/models.json`
registering `exo` as a custom provider:
     - `baseUrl` → `<apiUrl>/v1`
     - `api` → `openai-completions`
     - `apiKey` → `"exo"` (placeholder; exo ignores it)
- `compat.supportsDeveloperRole: false` and
`compat.supportsReasoningEffort: false`, per pi docs recommendation for
local OpenAI-compatible servers
- Auto-populates every running model with `id`, `contextWindow` (from
`/v1/models`), and `input: ["text", "image"]` for vision-capable models
- **Shell Command card** — `pi --provider exo --model <model>` for quick
launch.

The tab gracefully falls back to `your-model-id` when no models are
running, matching the behavior of the other tabs.

   ## Usage

   1. `npm install -g @mariozechner/pi-coding-agent`
   2. Paste the generated config into `~/.pi/agent/models.json`
3. Run `pi` and pick an exo model via `/model` — or run the shell
command directly

   ## Changes

- `dashboard/src/routes/integrations/+page.svelte` — adds `"Pi"` to the
`tabs` tuple, `piModel` state, `piModelsJson` + `piShellCommand`
derivations, and the tab content block.

   Single-file, scoped change — no backend or type changes.

   ## Testing

   - `cd dashboard && npm run build` — ✅ builds cleanly
   - `svelte-check` on the edited file — no new errors
- Manually verified the tab renders, the model selector updates the
generated JSON, and the config reflects `/v1/models` capabilities
(vision → `input: ["text","image"]`,
 `context_length` → `contextWindow`).

   ## Screenshots

<img width="1545" height="1236" alt="pi-tab"
src="https://github.com/user-attachments/assets/38aa179f-4ed9-4a1e-9783-d3baa7738263"
/>

Files touched

Diff

commit 4939fbe9952b187dc1adf34efce234f36413b6cc
Author: Nadeem Hilal Wani <nadeem.09wani@gmail.com>
Date:   Wed Apr 22 21:29:47 2026 +0400

    feat(dashboard): add Pi integration tab (#1925)
    
    ## Summary
    Adds a new **Pi** tab to the Integrations page (`/#/integrations`)
    alongside the existing Claude Code, OpenCode, Codex, OpenClaw, Open
    WebUI, n8n, and Firefox tabs.
    
    [pi](https://pi.dev) (`@mariozechner/pi-coding-agent`) is a terminal
    coding agent that supports custom OpenAI-compatible providers via
    `~/.pi/agent/models.json`.
    This tab gives users a copy-pasteable config to wire pi up to their exo
    cluster.
    
    ## What's in the tab
    - **Model selector** (shown when multiple models are running) — picks
    the default model for the generated shell command.
    - **Models Config card** — generates `~/.pi/agent/models.json`
    registering `exo` as a custom provider:
         - `baseUrl` → `<apiUrl>/v1`
         - `api` → `openai-completions`
         - `apiKey` → `"exo"` (placeholder; exo ignores it)
    - `compat.supportsDeveloperRole: false` and
    `compat.supportsReasoningEffort: false`, per pi docs recommendation for
    local OpenAI-compatible servers
    - Auto-populates every running model with `id`, `contextWindow` (from
    `/v1/models`), and `input: ["text", "image"]` for vision-capable models
    - **Shell Command card** — `pi --provider exo --model <model>` for quick
    launch.
    
    The tab gracefully falls back to `your-model-id` when no models are
    running, matching the behavior of the other tabs.
    
       ## Usage
    
       1. `npm install -g @mariozechner/pi-coding-agent`
       2. Paste the generated config into `~/.pi/agent/models.json`
    3. Run `pi` and pick an exo model via `/model` — or run the shell
    command directly
    
       ## Changes
    
    - `dashboard/src/routes/integrations/+page.svelte` — adds `"Pi"` to the
    `tabs` tuple, `piModel` state, `piModelsJson` + `piShellCommand`
    derivations, and the tab content block.
    
       Single-file, scoped change — no backend or type changes.
    
       ## Testing
    
       - `cd dashboard && npm run build` — ✅ builds cleanly
       - `svelte-check` on the edited file — no new errors
    - Manually verified the tab renders, the model selector updates the
    generated JSON, and the config reflects `/v1/models` capabilities
    (vision → `input: ["text","image"]`,
     `context_length` → `contextWindow`).
    
       ## Screenshots
    
    <img width="1545" height="1236" alt="pi-tab"
    src="https://github.com/user-attachments/assets/38aa179f-4ed9-4a1e-9783-d3baa7738263"
    />
---
 dashboard/src/routes/integrations/+page.svelte | 79 ++++++++++++++++++++++++++
 package-lock.json                              |  6 ++
 2 files changed, 85 insertions(+)

diff --git a/dashboard/src/routes/integrations/+page.svelte b/dashboard/src/routes/integrations/+page.svelte
index 924cfdf7..95a646e1 100644
--- a/dashboard/src/routes/integrations/+page.svelte
+++ b/dashboard/src/routes/integrations/+page.svelte
@@ -88,10 +88,12 @@
   let codexModel = $state("");
   let codexMcpPath = $state("/Users/username");
   let openClawModel = $state("");
+  let piModel = $state("");
   $effect(() => {
     const def = modelsBySize.length > 0 ? modelsBySize[0] : "your-model-id";
     codexModel = def;
     openClawModel = def;
+    piModel = def;
   });
 
   const claudeShellCommand = $derived(
@@ -218,6 +220,55 @@
     ),
   );
 
+  const piModelsJson = $derived.by(() => {
+    const models: Record<string, unknown>[] = [];
+    for (const modelId of runningModels) {
+      const caps = modelCapabilities[modelId] || [];
+      const ctxLen = modelContextLengths[modelId] || 0;
+      const entry: Record<string, unknown> = { id: modelId };
+      if (caps.includes("vision")) {
+        entry.input = ["text", "image"];
+      }
+      // Mark thinking-capable models so pi surfaces its thinking-level selector
+      // for them. exo capability strings: "thinking" (model emits reasoning
+      // content) and "thinking_toggle" (user can turn it on/off).
+      if (caps.includes("thinking") || caps.includes("thinking_toggle")) {
+        entry.reasoning = true;
+      }
+      if (ctxLen > 0) {
+        entry.contextWindow = ctxLen;
+      }
+      models.push(entry);
+    }
+    if (models.length === 0) {
+      models.push({ id: "your-model-id" });
+    }
+    return JSON.stringify(
+      {
+        providers: {
+          exo: {
+            baseUrl: `${apiUrl}/v1`,
+            api: "openai-completions",
+            apiKey: "exo",
+            compat: {
+              supportsDeveloperRole: false,
+              // exo's OpenAI surface takes a boolean `enable_thinking` toggle,
+              // not graded effort levels, so disable pi's `reasoning_effort`
+              // parameter and use the matching top-level-boolean format.
+              supportsReasoningEffort: false,
+              thinkingFormat: "qwen",
+            },
+            models,
+          },
+        },
+      },
+      null,
+      2,
+    );
+  });
+
+  const piShellCommand = $derived(`pi --provider exo --model ${piModel}`);
+
   const ollamaCommand = $derived(
     `OLLAMA_HOST=${apiUrl}/ollama ollama run ${modelsBySize.length > 0 ? modelsBySize[0] : "your-model-id"}`,
   );
@@ -277,6 +328,7 @@
     "OpenCode",
     "Codex",
     "OpenClaw",
+    "Pi",
     "Open WebUI",
     "n8n",
     "Firefox",
@@ -515,6 +567,33 @@
           config={`openclaw doctor --fix${(modelCapabilities[openClawModel] || []).includes("vision") ? `\nopenclaw models set-image exo/${openClawModel}` : ""}\nopenclaw gateway &\nopenclaw dashboard`}
           language="bash"
         />
+      {:else if activeTab === "Pi"}
+        {#if runningModels.length > 1}
+          <div class="text-xs">
+            <span
+              class="text-exo-light-gray/50 text-[10px] uppercase tracking-wider block mb-1"
+              >Model</span
+            >
+            <select bind:value={piModel} class={selectClass}>
+              {#each runningModels as model}
+                <option value={model}>{model.split("/").pop()}</option>
+              {/each}
+            </select>
+          </div>
+        {/if}
+        <IntegrationCard
+          title="Models Config"
+          subtitle="~/.pi/agent/models.json"
+          description="Register exo as a custom provider in pi. Create or edit this file, then run pi and pick an exo model via /model. Install pi with: npm install -g @mariozechner/pi-coding-agent"
+          config={piModelsJson}
+        />
+        <IntegrationCard
+          title="Shell Command"
+          subtitle="Run in terminal"
+          description="Launch pi directly with the exo provider and model selected."
+          config={piShellCommand}
+          language="bash"
+        />
       {:else if activeTab === "Open WebUI"}
         <IntegrationCard
           title="1. Start Open WebUI"
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..6768af15
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,6 @@
+{
+  "name": "exo",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}

← 73782ecc Fix event mutation causing indexed vs event mismatch (#1964)  ·  back to Exo  ·  feat(app): add friendly context message to bug report prompt 8993ccaf →