pi-ollama-cloud 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/index.ts +471 -0
  4. package/package.json +23 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fabio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # pi-ollama-cloud
2
+
3
+ Ollama Cloud provider plugin for [Pi](https://github.com/badlogic/pi-mono) coding agent.
4
+
5
+ Registers Ollama Cloud as a model provider with dynamically fetched models, and provides `ollama_web_search` and `ollama_web_fetch` tools that use the [Ollama Cloud web search API](https://docs.ollama.com/capabilities/web-search) — no local Ollama server required.
6
+
7
+ ## Features
8
+
9
+ - **Dynamic model discovery** - Fetches the full model list from `ollama.com/v1/models`, then fetches per-model details via `/api/show` to determine capabilities, context length, and tool support.
10
+ - **Persistent cache** - Raw API responses are cached at `~/.pi/agent/cache/ollama-cloud-models.json` so models are available immediately on startup without hitting the network.
11
+ - **Cold cache fallback** - When no cache exists, a small set of hardcoded models is used until `/ollama-cloud-refresh` is run.
12
+ - **`/ollama-cloud-refresh` command** - Re-fetches the model list from the API and updates the cache and provider registration live (no restart needed).
13
+ - **`ollama_web_search` tool** - Search the web for real-time information using Ollama Cloud's `/api/web_search` endpoint. Returns titles, URLs, and content snippets.
14
+ - **`ollama_web_fetch` tool** - Fetch and extract text content from a web page URL using Ollama Cloud's `/api/web_fetch` endpoint. Returns page title, content, and links.
15
+ - **Zero cost tracking** - All models are registered with zero costs since Ollama Cloud uses a flat subscription model (Free, Pro, Max) rather than per-token billing. Per-request costs don't apply, so Pi's cost tracker always shows zero. See [ollama.com/pricing](https://ollama.com/pricing) for plan details.
16
+
17
+ ## Prerequisites
18
+
19
+ - An [Ollama Cloud API key](https://ollama.com)
20
+
21
+ ## Installation
22
+
23
+ ### Option 1: from npm (recommended)
24
+
25
+ ```bash
26
+ pi install npm:pi-ollama-cloud
27
+ ```
28
+
29
+ This installs the latest published version from npm. Run `pi update` to get new versions.
30
+
31
+ ### Option 2: from git
32
+
33
+ ```bash
34
+ pi install git:github.com/fgrehm/pi-ollama-cloud
35
+ ```
36
+
37
+ This clones the repo to `~/.pi/agent/git/` and adds it to your settings.
38
+
39
+ For project-local install (stored in `.pi/git/`):
40
+
41
+ ```bash
42
+ pi install git:github.com/fgrehm/pi-ollama-cloud --local
43
+ ```
44
+
45
+ ### Option 3: `-e` flag (try without installing)
46
+
47
+ ```bash
48
+ pi -e npm:pi-ollama-cloud
49
+ ```
50
+
51
+ ### Option 4: Clone manually (if you want to make changes and "try it live")
52
+
53
+ Pi auto-discovers subdirectories under `~/.pi/agent/extensions/`:
54
+
55
+ ```bash
56
+ git clone git@github.com:fgrehm/pi-ollama-cloud.git ~/.pi/agent/extensions/pi-ollama-cloud
57
+ ```
58
+
59
+ ## Setup
60
+
61
+ ### 1. Get an API key
62
+
63
+ Sign up at [ollama.com](https://ollama.com) and generate an API key.
64
+
65
+ ### 2. Configure the API key
66
+
67
+ Either set the `OLLAMA_API_KEY` environment variable:
68
+
69
+ ```bash
70
+ export OLLAMA_API_KEY="your-key"
71
+ ```
72
+
73
+ Or add it to `~/.pi/agent/auth.json`:
74
+
75
+ ```json
76
+ {
77
+ "ollama-cloud": {
78
+ "type": "api_key",
79
+ "key": "your-key"
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### 3. Fetch models
85
+
86
+ On first launch the plugin will use a small set of fallback models. Run:
87
+
88
+ ```
89
+ /ollama-cloud-refresh
90
+ ```
91
+
92
+ This fetches the full model list from the Ollama Cloud API and caches it locally.
93
+
94
+ ### 4. Select a model
95
+
96
+ Use `/model` or `Ctrl+L` to switch to an Ollama Cloud model. Models appear under the `ollama-cloud` provider.
97
+
98
+ ## How it works
99
+
100
+ The plugin uses two Ollama Cloud API endpoints to build the model list:
101
+
102
+ 1. **`GET https://ollama.com/v1/models`** - Returns a list of all available model IDs.
103
+ 2. **`POST https://ollama.com/api/show`** - For each model, fetches details including capabilities (`tools`, `thinking`, `vision`) and context length.
104
+
105
+ Only models with the `tools` capability are registered - these are the ones Pi can use for tool-calling.
106
+
107
+ The raw `/api/show` responses are cached at `~/.pi/agent/cache/ollama-cloud-models.json`. This cache **never expires** - run `/ollama-cloud-refresh` to update it.
108
+
109
+ Model metadata is derived from the cached data:
110
+
111
+ | Field | Source |
112
+ |---|---|
113
+ | `reasoning` | `capabilities` includes `"thinking"` |
114
+ | `input` | `["text", "image"]` if `capabilities` includes `"vision"`, else `["text"]` |
115
+ | `contextWindow` | `model_info.*.context_length` (falls back to 128000) |
116
+ | `maxTokens` | Fixed at 32768 |
117
+ | `cost` | All zeros (Ollama Cloud uses subscription plans, not per-token billing - see [pricing](https://ollama.com/pricing)) |
118
+
119
+ ## Tools
120
+
121
+ | Tool | Description |
122
+ |---|---|
123
+ | `ollama_web_search` | Search the web via Ollama Cloud's `/api/web_search` |
124
+ | `ollama_web_fetch` | Fetch a web page via Ollama Cloud's `/api/web_fetch` |
125
+
126
+ Both tools use the same Ollama Cloud API key configured for the provider. No local Ollama server is needed.
127
+
128
+ ## Commands
129
+
130
+ | Command | Description |
131
+ |---|---|
132
+ | `/ollama-cloud-refresh` | Fetch models from the Ollama Cloud API, update cache, and re-register the provider |
133
+
134
+ ## Development
135
+
136
+ ```bash
137
+ npm install # install devDependencies (biome)
138
+ npm run check # lint + format with auto-fix
139
+ npm run lint # lint only (no fixes)
140
+ npm run format # format only
141
+ ```
142
+
143
+ The project uses [Biome](https://biomejs.dev/) for linting and formatting (2-space indent, line width 120).
144
+
145
+ ## How is this different from `ollama launch pi`?
146
+
147
+ [`ollama launch pi`](https://docs.ollama.com/integrations/pi) is Ollama's built-in one-command setup that configures Pi to talk to your **local Ollama server**. Both local and cloud models work - cloud models (e.g. `qwen3.5:cloud`) are proxied through your local server to `ollama.com`. This extension takes a different approach: it connects Pi **directly** to Ollama's hosted API at `ollama.com`, bypassing the local server entirely.
148
+
149
+ | | `ollama launch pi` | `pi-ollama-cloud` |
150
+ |---|---|---|
151
+ | **Provider name** | `ollama` | `ollama-cloud` |
152
+ | **Endpoint** | Local Ollama server (`http://localhost:11434/v1`) | Ollama Cloud (`https://ollama.com/v1`) |
153
+ | **Local models** | ✅ Run on your machine | ❌ Not available |
154
+ | **Cloud models** | ✅ Proxied through local server (e.g. `qwen3.5:cloud`) | ✅ Connected directly |
155
+ | **Local Ollama required?** | Yes - must be installed and running | No - works without any local server |
156
+ | **Authentication** | Handled by the local server (sign-in flow via `ollama`) | Ollama Cloud API key (set via `OLLAMA_API_KEY` or `auth.json`) |
157
+ | **Model discovery** | Interactive picker with curated recommendations + pulled models | Dynamic - fetches all available cloud models with tool support from the API |
158
+ | **Web tools** | Auto-installed (`@ollama/pi-web-search`) when cloud is enabled | ✅ Built-in: `ollama_web_search` and `ollama_web_fetch` use the [Ollama Cloud web search API](https://docs.ollama.com/capabilities/web-search) directly (same API key, no local server needed) |
159
+ | **Setup effort** | One command: `ollama launch pi` | Install extension + API key + `/ollama-cloud-refresh` |
160
+ | **Use when** | You're already running Ollama locally and want the default experience | You don't want to run a local server, or want a standalone cloud-only provider alongside your local setup |
161
+
162
+ **You can use both at the same time.** The providers live under different names (`ollama` vs `ollama-cloud`), so you can switch between them with `/model` or `Ctrl+L`. For example, use your local `ollama` provider for low-latency work on smaller models, and `ollama-cloud` for direct access to the full catalog of cloud models without needing a local server.
163
+
164
+ > **Note:** The [`@ollama/pi-web-search`](https://www.npmjs.com/package/@ollama/pi-web-search) package (installed automatically by `ollama launch pi`) calls the **local** Ollama server's `/api/experimental/web_search` and `/api/experimental/web_fetch` endpoints and authenticates via `ollama signin`. This extension's `ollama_web_search` and `ollama_web_fetch` tools use the **cloud** API at `ollama.com/api/web_search` and `ollama.com/api/web_fetch` instead — same API key, no local server required. Both can coexist: the local tools register as `web_search`/`web_fetch` and these register as `ollama_web_search`/`ollama_web_fetch` to avoid name conflicts.
165
+
166
+ ## Releasing
167
+
168
+ Publishing a new version to npm is a two-command process:
169
+
170
+ ```bash
171
+ # 1. Bump version and create a git tag in one step
172
+ npm version minor # or patch, or major
173
+ # 2. Push the tag to trigger the GitHub Actions publish workflow
174
+ git push --tags
175
+ ```
176
+
177
+ The tag version must match the version in `package.json` — `npm version` handles this automatically. The workflow at `.github/workflows/publish.yml` verifies the match before publishing to npm.
178
+
179
+ The workflow uses npm's [trusted publishing](https://docs.npmjs.com/trusted-publishers/) (OIDC) — no tokens stored as secrets. To set it up:
180
+
181
+ 1. Go to [npmjs.com](https://www.npmjs.com) → your avatar → **Packages** → `pi-ollama-cloud` → **Settings** → **Trusted publishing**
182
+ 2. Click **GitHub Actions** and enter:
183
+ - **Workflow filename**: `publish.yml`
184
+ 3. Save
185
+
186
+ Each publish also gets automatic [provenance attestation](https://docs.npmjs.com/generating-provenance-statements).
187
+
188
+ ## Notes
189
+
190
+ - Some Ollama Cloud models may reject the `developer` message role, causing a `400` error. If you encounter this, the model may need `compat: { supportsDeveloperRole: false }`. You can edit `index.ts` to add this for specific models, or open an issue to track it.
191
+ - The fetch timeout is 10 seconds per request. On slow connections, some model detail fetches may time out - the plugin reports how many succeeded vs failed.
package/index.ts ADDED
@@ -0,0 +1,471 @@
1
+ /**
2
+ * Ollama Cloud Provider Extension
3
+ *
4
+ * Registers Ollama Cloud as a model provider with dynamically fetched models.
5
+ *
6
+ * Setup:
7
+ * 1. Get an API key from https://ollama.com
8
+ * 2. Add to auth.json in the agent config dir (~/.pi/agent/auth.json, or set PI_CODING_AGENT_DIR):
9
+ * { "ollama-cloud": { "type": "api_key", "key": "your-key" } }
10
+ * 3. Run /ollama-cloud-refresh to fetch models (uses cache or fallback on boot)
11
+ * 4. Use /model or ctrl+l to select an Ollama Cloud model
12
+ *
13
+ * Two endpoints are used to build the model list:
14
+ * - GET https://ollama.com/v1/models → list of model IDs
15
+ * - POST https://ollama.com/api/show → per-model details (capabilities, context length)
16
+ *
17
+ * Raw /api/show responses are cached at <agentDir>/cache/ollama-cloud-models.json
18
+ * so the provider assembly can be debugged and re-derived without re-fetching.
19
+ *
20
+ * Cache never expires — run /ollama-cloud-refresh to update.
21
+ * Cold cache falls back to a small set of hardcoded models.
22
+ *
23
+ * Only models with "tools" capability are registered.
24
+ */
25
+
26
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
27
+ import { join } from "node:path";
28
+ import {
29
+ type ExtensionAPI,
30
+ type ExtensionCommandContext,
31
+ type ExtensionContext,
32
+ type ProviderModelConfig,
33
+ getAgentDir,
34
+ keyHint,
35
+ truncateToVisualLines,
36
+ } from "@mariozechner/pi-coding-agent";
37
+ import { Text, truncateToWidth } from "@mariozechner/pi-tui";
38
+ import { Type } from "@sinclair/typebox";
39
+
40
+ const CACHE_DIR = join(getAgentDir(), "cache");
41
+ const CACHE_FILE = join(CACHE_DIR, "ollama-cloud-models.json");
42
+ const FETCH_TIMEOUT_MS = 10000;
43
+
44
+ /**
45
+ * Base URL for the Ollama Cloud API.
46
+ * Defaults to "https://ollama.com"; override with OLLAMA_API_BASE to point at a proxy or self-hosted instance.
47
+ */
48
+ const OLLAMA_BASE = process.env.OLLAMA_API_BASE || "https://ollama.com";
49
+
50
+ // --- Raw API types ---
51
+
52
+ /** Response from POST /api/show */
53
+ interface OllamaShowResponse {
54
+ details: {
55
+ parent_model: string;
56
+ format: string;
57
+ family: string;
58
+ families: string[] | null;
59
+ parameter_size: string;
60
+ quantization_level: string;
61
+ };
62
+ model_info: Record<string, unknown>;
63
+ capabilities: string[];
64
+ modified_at: string;
65
+ }
66
+
67
+ /** On-disk cache: raw /api/show responses keyed by model ID */
68
+ interface CachedData {
69
+ timestamp: number;
70
+ models: Record<string, OllamaShowResponse>;
71
+ }
72
+
73
+ // --- Assembly: raw API data → ProviderModelConfig[] ---
74
+
75
+ function getContextLength(modelInfo: Record<string, unknown>): number {
76
+ for (const [key, value] of Object.entries(modelInfo)) {
77
+ if (key.endsWith(".context_length") && typeof value === "number") {
78
+ return value;
79
+ }
80
+ }
81
+ return 128000;
82
+ }
83
+
84
+ function assembleModels(raw: Record<string, OllamaShowResponse>): ProviderModelConfig[] {
85
+ return Object.entries(raw)
86
+ .filter(([, data]) => data.capabilities?.includes("tools"))
87
+ .map(([id, data]) => ({
88
+ id,
89
+ name: id,
90
+ reasoning: data.capabilities?.includes("thinking") ?? false,
91
+ input: (data.capabilities?.includes("vision") ? ["text", "image"] : ["text"]) as ("text" | "image")[],
92
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
93
+ contextWindow: getContextLength(data.model_info ?? {}),
94
+ maxTokens: 32768,
95
+ }));
96
+ }
97
+
98
+ // --- Fallback models (cold cache) ---
99
+
100
+ const FALLBACK_MODELS: ProviderModelConfig[] = [
101
+ {
102
+ id: "glm-5.1:cloud",
103
+ name: "GLM 5.1 Cloud",
104
+ reasoning: true,
105
+ input: ["text"],
106
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
107
+ contextWindow: 202752,
108
+ maxTokens: 32768,
109
+ },
110
+ {
111
+ id: "gemma4:cloud",
112
+ name: "Gemma 4 Cloud",
113
+ reasoning: true,
114
+ input: ["text"],
115
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
116
+ contextWindow: 262144,
117
+ maxTokens: 32768,
118
+ },
119
+ ];
120
+
121
+ // --- Cache I/O ---
122
+
123
+ function readCache(): Record<string, OllamaShowResponse> | null {
124
+ try {
125
+ if (!existsSync(CACHE_FILE)) return null;
126
+ const data: CachedData = JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
127
+ if (!data.models || Object.keys(data.models).length === 0) return null;
128
+ return data.models;
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ function writeCache(models: Record<string, OllamaShowResponse>): void {
135
+ try {
136
+ mkdirSync(CACHE_DIR, { recursive: true });
137
+ writeFileSync(CACHE_FILE, JSON.stringify({ timestamp: Date.now(), models } satisfies CachedData, null, 2));
138
+ } catch {
139
+ // Ignore cache write errors
140
+ }
141
+ }
142
+
143
+ // --- API fetch ---
144
+
145
+ async function fetchModels(ctx: ExtensionCommandContext): Promise<Record<string, OllamaShowResponse>> {
146
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider("ollama-cloud");
147
+ if (!apiKey) {
148
+ ctx.ui.notify("No Ollama Cloud API key configured (auth.json or OLLAMA_API_KEY env var)", "error");
149
+ return {};
150
+ }
151
+
152
+ // 1. Fetch model list from /v1/models
153
+ const listController = new AbortController();
154
+ const listTimeout = setTimeout(() => listController.abort(), FETCH_TIMEOUT_MS);
155
+ try {
156
+ const res = await fetch(`${OLLAMA_BASE}/v1/models`, {
157
+ headers: { Authorization: `Bearer ${apiKey}` },
158
+ signal: listController.signal,
159
+ });
160
+ if (!res.ok) {
161
+ ctx.ui.notify(`Failed to fetch model list: ${res.status}`, "error");
162
+ return {};
163
+ }
164
+ const data = (await res.json()) as { data: { id: string }[] };
165
+ const modelIds = data.data.map((m) => m.id);
166
+ ctx.ui.notify(`Found ${modelIds.length} models, fetching details...`);
167
+ clearTimeout(listTimeout);
168
+
169
+ // 2. Fetch /api/show for each model in parallel
170
+ const results: Record<string, OllamaShowResponse> = {};
171
+ const settled = await Promise.allSettled(
172
+ modelIds.map(async (id) => {
173
+ const showController = new AbortController();
174
+ const showTimeout = setTimeout(() => showController.abort(), FETCH_TIMEOUT_MS);
175
+ try {
176
+ const res = await fetch(`${OLLAMA_BASE}/api/show`, {
177
+ method: "POST",
178
+ headers: {
179
+ Authorization: `Bearer ${apiKey}`,
180
+ "Content-Type": "application/json",
181
+ },
182
+ body: JSON.stringify({ model: id }),
183
+ signal: showController.signal,
184
+ });
185
+ if (!res.ok) return;
186
+ const showData = (await res.json()) as OllamaShowResponse;
187
+ results[id] = showData;
188
+ } finally {
189
+ clearTimeout(showTimeout);
190
+ }
191
+ }),
192
+ );
193
+
194
+ const succeeded = settled.filter((r) => r.status === "fulfilled").length;
195
+ const failed = settled.length - succeeded;
196
+ ctx.ui.notify(`Fetched ${Object.keys(results).length} model details${failed ? ` (${failed} failed)` : ""}`, "info");
197
+
198
+ return results;
199
+ } catch {
200
+ ctx.ui.notify("Failed to fetch Ollama Cloud models", "error");
201
+ return {};
202
+ }
203
+ }
204
+
205
+ // --- Web search/fetch types ---
206
+
207
+ interface SearchResponse {
208
+ results: Array<{
209
+ title: string;
210
+ url: string;
211
+ content: string;
212
+ }>;
213
+ }
214
+
215
+ interface FetchResponse {
216
+ title: string;
217
+ content: string;
218
+ links: string[];
219
+ }
220
+
221
+ async function getCloudApiKey(ctx: ExtensionContext): Promise<string | undefined> {
222
+ return ctx.modelRegistry.getApiKeyForProvider("ollama-cloud");
223
+ }
224
+
225
+ // --- Tool rendering helpers ---
226
+
227
+ const PREVIEW_LINES = 8;
228
+
229
+ /**
230
+ * Build a renderResult handler that shows a truncated preview when collapsed
231
+ * and the full output when expanded. Follows the bash tool pattern.
232
+ */
233
+ function createRenderResult() {
234
+ return (
235
+ result: { content: Array<{ type: string; text: string }>; isError?: boolean },
236
+ options: { expanded: boolean; isPartial: boolean },
237
+ theme: import("@mariozechner/pi-coding-agent").Theme,
238
+ context: {
239
+ invalidate: () => void;
240
+ lastComponent: import("@mariozechner/pi-tui").Component | undefined;
241
+ state: { cachedWidth?: number; cachedLines?: string[]; cachedSkipped?: number };
242
+ },
243
+ ) => {
244
+ const state = context.state;
245
+ const output = result.content
246
+ .map((c) => c.text)
247
+ .join("")
248
+ .trim();
249
+ const styledOutput = output
250
+ .split("\n")
251
+ .map((line: string) => theme.fg("toolOutput", line))
252
+ .join("\n");
253
+
254
+ if (options.expanded || result.isError) {
255
+ const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
256
+ text.setText(result.isError ? styledOutput : `\n${styledOutput}`);
257
+ return text;
258
+ }
259
+
260
+ return {
261
+ render: (width: number) => {
262
+ if (state.cachedWidth !== width) {
263
+ const preview = truncateToVisualLines(styledOutput, PREVIEW_LINES, width);
264
+ state.cachedLines = preview.visualLines;
265
+ state.cachedSkipped = preview.skippedCount;
266
+ state.cachedWidth = width;
267
+ }
268
+ if (state.cachedSkipped && state.cachedSkipped > 0) {
269
+ const hint =
270
+ theme.fg("muted", `... (${state.cachedSkipped} earlier lines,`) +
271
+ ` ${keyHint("app.tools.expand", "to expand")})`;
272
+ return ["", truncateToWidth(hint, width, "..."), ...(state.cachedLines ?? [])];
273
+ }
274
+ return ["", ...(state.cachedLines ?? [])];
275
+ },
276
+ invalidate: () => {
277
+ state.cachedWidth = undefined;
278
+ state.cachedLines = undefined;
279
+ state.cachedSkipped = undefined;
280
+ },
281
+ };
282
+ };
283
+ }
284
+
285
+ // --- Main ---
286
+
287
+ export default async function (pi: ExtensionAPI) {
288
+ // Boot: assemble from cache or fall back
289
+ const cached = readCache();
290
+ const models = cached ? assembleModels(cached) : FALLBACK_MODELS;
291
+
292
+ pi.registerProvider("ollama-cloud", {
293
+ baseUrl: `${OLLAMA_BASE}/v1`,
294
+ apiKey: "OLLAMA_API_KEY",
295
+ api: "openai-completions",
296
+ models,
297
+ });
298
+
299
+ // Slash command to refresh model list
300
+ pi.registerCommand("ollama-cloud-refresh", {
301
+ description: "Refresh Ollama Cloud models from the API",
302
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
303
+ ctx.ui.setWorkingMessage("Refreshing Ollama Cloud models...");
304
+
305
+ const raw = await fetchModels(ctx);
306
+ if (Object.keys(raw).length === 0) {
307
+ ctx.ui.notify("No models fetched — keeping existing models", "warning");
308
+ ctx.ui.setWorkingMessage();
309
+ return;
310
+ }
311
+
312
+ writeCache(raw);
313
+ const newModels = assembleModels(raw);
314
+
315
+ // NOTE: Some models may trigger errors like:
316
+ // Error: 400 "developer is not one of ['system', 'assistant', 'user', 'tool', 'function']"
317
+ // If that comes up, consider setting `supportsDeveloperRole: false` in the compat field
318
+ // for the provider or specific models, e.g.:
319
+ // compat: { supportsDeveloperRole: false }
320
+ pi.registerProvider("ollama-cloud", {
321
+ baseUrl: `${OLLAMA_BASE}/v1`,
322
+ apiKey: "OLLAMA_API_KEY",
323
+ api: "openai-completions",
324
+ models: newModels,
325
+ });
326
+
327
+ ctx.ui.notify(`Registered ${newModels.length} Ollama Cloud models`, "info");
328
+ ctx.ui.setWorkingMessage();
329
+ },
330
+ });
331
+
332
+ // Web search tool — uses Ollama Cloud API (no local server needed)
333
+ pi.registerTool({
334
+ name: "ollama_web_search",
335
+ label: "Ollama Web Search",
336
+ description:
337
+ "Search the web for real-time information using Ollama Cloud's web search API. " +
338
+ "Returns relevant results with titles, URLs, and content snippets. " +
339
+ "Requires an Ollama Cloud API key.",
340
+ parameters: Type.Object({
341
+ query: Type.String({ description: "The search query to execute" }),
342
+ max_results: Type.Optional(
343
+ Type.Number({ description: "Maximum number of search results to return (default: 5, max: 10)", default: 5 }),
344
+ ),
345
+ }),
346
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
347
+ const apiKey = await getCloudApiKey(ctx);
348
+ if (!apiKey) {
349
+ return {
350
+ content: [
351
+ {
352
+ type: "text",
353
+ text: "Error: No Ollama Cloud API key configured. Set OLLAMA_API_KEY or add to auth.json.",
354
+ },
355
+ ],
356
+ isError: true,
357
+ };
358
+ }
359
+
360
+ try {
361
+ const res = await fetch(`${OLLAMA_BASE}/api/web_search`, {
362
+ method: "POST",
363
+ headers: {
364
+ Authorization: `Bearer ${apiKey}`,
365
+ "Content-Type": "application/json",
366
+ },
367
+ body: JSON.stringify({
368
+ query: params.query,
369
+ max_results: params.max_results ?? 5,
370
+ }),
371
+ signal,
372
+ });
373
+
374
+ if (!res.ok) {
375
+ const errorText = await res.text().catch(() => "");
376
+ return {
377
+ content: [
378
+ { type: "text", text: `Search API error (status ${res.status}): ${errorText || res.statusText}` },
379
+ ],
380
+ isError: true,
381
+ };
382
+ }
383
+
384
+ const data = (await res.json()) as SearchResponse;
385
+ const formatted = data.results
386
+ .map((r, i) => `${i + 1}. ${r.title}\n URL: ${r.url}\n ${r.content}`)
387
+ .join("\n\n");
388
+
389
+ return {
390
+ content: [{ type: "text", text: formatted || "No results found." }],
391
+ details: { results: data.results },
392
+ };
393
+ } catch (err) {
394
+ return {
395
+ content: [{ type: "text", text: `Web search failed: ${err instanceof Error ? err.message : String(err)}` }],
396
+ isError: true,
397
+ };
398
+ }
399
+ },
400
+ renderResult: createRenderResult(),
401
+ });
402
+
403
+ // Web fetch tool — uses Ollama Cloud API (no local server needed)
404
+ pi.registerTool({
405
+ name: "ollama_web_fetch",
406
+ label: "Ollama Web Fetch",
407
+ description:
408
+ "Fetch and extract text content from a web page URL using Ollama Cloud's web fetch API. " +
409
+ "Returns the page title, main content, and links found on the page. " +
410
+ "Requires an Ollama Cloud API key.",
411
+ parameters: Type.Object({
412
+ url: Type.String({ description: "URL to fetch and extract content from" }),
413
+ }),
414
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
415
+ const apiKey = await getCloudApiKey(ctx);
416
+ if (!apiKey) {
417
+ return {
418
+ content: [
419
+ {
420
+ type: "text",
421
+ text: "Error: No Ollama Cloud API key configured. Set OLLAMA_API_KEY or add to auth.json.",
422
+ },
423
+ ],
424
+ isError: true,
425
+ };
426
+ }
427
+
428
+ try {
429
+ const res = await fetch(`${OLLAMA_BASE}/api/web_fetch`, {
430
+ method: "POST",
431
+ headers: {
432
+ Authorization: `Bearer ${apiKey}`,
433
+ "Content-Type": "application/json",
434
+ },
435
+ body: JSON.stringify({ url: params.url }),
436
+ signal,
437
+ });
438
+
439
+ if (!res.ok) {
440
+ const errorText = await res.text().catch(() => "");
441
+ return {
442
+ content: [{ type: "text", text: `Fetch API error (status ${res.status}): ${errorText || res.statusText}` }],
443
+ isError: true,
444
+ };
445
+ }
446
+
447
+ const data = (await res.json()) as FetchResponse;
448
+ const formatted = [
449
+ `Title: ${data.title}`,
450
+ "",
451
+ "Content:",
452
+ data.content,
453
+ "",
454
+ `Links found: ${data.links?.length ?? 0}`,
455
+ ...(data.links?.slice(0, 10).map((l) => ` - ${l}`) ?? []),
456
+ ].join("\n");
457
+
458
+ return {
459
+ content: [{ type: "text", text: formatted }],
460
+ details: { title: data.title, content: data.content, links: data.links },
461
+ };
462
+ } catch (err) {
463
+ return {
464
+ content: [{ type: "text", text: `Web fetch failed: ${err instanceof Error ? err.message : String(err)}` }],
465
+ isError: true,
466
+ };
467
+ }
468
+ },
469
+ renderResult: createRenderResult(),
470
+ });
471
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "pi-ollama-cloud",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "keywords": ["pi-package"],
6
+ "files": ["index.ts", "README.md", "LICENSE"],
7
+ "scripts": {
8
+ "check": "biome check --write .",
9
+ "lint": "biome check .",
10
+ "format": "biome format --write ."
11
+ },
12
+ "pi": {
13
+ "extensions": ["./index.ts"]
14
+ },
15
+ "peerDependencies": {
16
+ "@mariozechner/pi-coding-agent": "*",
17
+ "@mariozechner/pi-tui": "*",
18
+ "@sinclair/typebox": "*"
19
+ },
20
+ "devDependencies": {
21
+ "@biomejs/biome": "2"
22
+ }
23
+ }