pi-ollama-cloud 0.4.1 → 0.5.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.5.0] - 2026-05-22
6
+
7
+ - Update `peerDependencies` and imports to the new `@earendil-works/*` packages.
8
+ - Add build script (`scripts/generate-models.ts`) to generate `models.generated.ts` with Ollama Cloud tool-capable models from live API data. The script requires no API key.
9
+ - Drop `FALLBACK_MODELS` in favor of `GENERATED_MODELS` which ships with the package.
10
+ - Remove `apiKey` requirement from `fetchModelIds` and `fetchModelDetails`. Ollama Cloud's public `/v1/models` and `/api/show` endpoints do not require authentication for model metadata. (#15)
11
+ - Remove `getOllamaCloudApiKey`, `refreshModelsFromAuth`, and the "No API key found" notification path from `fetchModels`. Users can now browse model metadata without configuring a key.
12
+ - Remove `401`/`403` error handling from the model metadata fetch functions.
13
+ - Add configuration system (`config.ts`) with JSON config files (global + project-local) and `/ollama-webtools` command for runtime web tools toggling.
14
+ - Add test infrastructure (`vitest`) and model validation tests (`test/models.test.ts`).
15
+ - Add HTTP status-aware error handling in web tools (`401`/`403` → auth error, `429` → rate limit).
16
+ - Add explicit `buildCompat()` with all 17 `OpenAICompletionsCompat` flags set explicitly, verified against Ollama API docs.
17
+ - Set the provider display name to `Ollama Cloud` so it reads cleanly in `/login`, `/model`, and the model selector.
18
+ - Document `/login` as the recommended way to configure the API key.
19
+ - Update README to clarify baked-in model list and refresh behavior.
20
+ - Document web-tools.ts module boundaries in file header comment.
21
+
5
22
  ## [0.4.1] - 2026-05-07
6
23
 
7
24
  - Add `renderCall` to `ollama_web_search` and `ollama_web_fetch` tools so the TUI displays the query/URL in the tool call header instead of just the bare tool name. (#12)
package/README.md CHANGED
@@ -8,8 +8,9 @@ Registers Ollama Cloud as a model provider with dynamically fetched models, and
8
8
 
9
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
10
  - **Curated thinking levels** - Maps Pi's thinking levels to Ollama Cloud's OpenAI-compatible `reasoning_effort` values via `thinking-levels.ts`, with per-model exceptions based on API testing.
11
- - **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.
12
- - **Startup refresh** - When the local cache is stale, the plugin uses it immediately and then runs the same visible refresh flow as `/ollama-cloud-refresh` once the Pi session UI is available. Missing/invalid caches use a small fallback list until refresh completes.
11
+ - **Baked-in model list** - A generated model list (`models.generated.ts`) ships with the extension so models are available immediately on first launch without any network calls. Updated by running `npm run generate-models` and releasing a new version.
12
+ - **Persistent cache** - Running `/ollama-cloud-refresh` fetches the latest models from the API and caches them to `~/.pi/agent/cache/ollama-cloud-models.json`. On subsequent launches, this disk cache takes precedence over the baked-in list.
13
+ - **Auto-refresh on stale cache** - When the disk cache is older than 30 days, the extension uses it immediately and shows a visible refresh progress widget on the next `session_start` to pull in any new models.
13
14
  - **`/ollama-cloud-refresh` command** - Re-fetches the model list and updates the cache and provider registration live (no restart needed).
14
15
  - **`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.
15
16
  - **`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.
@@ -65,13 +66,15 @@ Sign up at [ollama.com](https://ollama.com) and generate an API key.
65
66
 
66
67
  ### 2. Configure the API key
67
68
 
68
- Either set the `OLLAMA_API_KEY` environment variable:
69
+ The simplest way is the `/login` command inside Pi: run `/login`, choose **Use an API key**, pick **Ollama Cloud**, and paste your key. Pi stores it in `~/.pi/agent/auth.json` and `/logout` removes it.
70
+
71
+ Alternatively, set the `OLLAMA_API_KEY` environment variable:
69
72
 
70
73
  ```bash
71
74
  export OLLAMA_API_KEY="your-key"
72
75
  ```
73
76
 
74
- Or add it to `~/.pi/agent/auth.json`:
77
+ Or add it to `~/.pi/agent/auth.json` by hand:
75
78
 
76
79
  ```json
77
80
  {
@@ -82,19 +85,36 @@ Or add it to `~/.pi/agent/auth.json`:
82
85
  }
83
86
  ```
84
87
 
85
- ### 3. Disable web tools (optional)
88
+ ### 3. Configure the extension (optional)
86
89
 
87
- If you want to use a different web search or fetch tool (e.g. Brave) by default, and need to avoid conflicts with the built-in Ollama Cloud tools, set the `PI_OLLAMA_WEB_TOOLS` environment variable to any falsy value:
90
+ Extension settings can be set via JSON config files. Project-local settings override global/user-level settings.
88
91
 
89
- ```bash
90
- export PI_OLLAMA_WEB_TOOLS=0
92
+ | Location | Scope |
93
+ |---|---|
94
+ | `~/.pi/agent/ollama-cloud.json` | Global / user-level (all projects) |
95
+ | `.pi/ollama-cloud.json` | Project-local (takes precedence) |
96
+
97
+ **Available settings:**
98
+
99
+ | Setting | Type | Default | Description |
100
+ |---|---|---|---|
101
+ | `webTools` | boolean | `true` | Set to `false` to prevent `ollama_web_search` and `ollama_web_fetch` from being registered |
102
+
103
+ Example `ollama-cloud.json`:
104
+
105
+ ```json
106
+ {
107
+ "webTools": false
108
+ }
91
109
  ```
92
110
 
93
- Accepted disabling values are `0`, `false`, `no`, `off`, or an empty string. When disabled, `ollama_web_search` and `ollama_web_fetch` are not registered. The model provider and `/ollama-cloud-refresh` command remain active regardless.
111
+ The `PI_OLLAMA_WEB_TOOLS` environment variable still works as an override above config files. Set it to `0`, `false`, `no`, or `off` to disable web tools regardless of config file settings.
112
+
113
+ ### 4. Fetch models (optional)
94
114
 
95
- ### 4. Fetch models
115
+ On first launch the plugin uses a baked-in model list shipped with the extension — no network calls needed. If you want the very latest models, run `/ollama-cloud-refresh` to fetch from the API and cache the result to disk. After that, the disk cache is used on subsequent launches.
96
116
 
97
- On first launch the plugin registers a small hardcoded fallback list, then refreshes model metadata automatically with the same progress widget used by the manual command. If an existing cache is merely stale, that cached model list remains active while refresh runs. You can also run:
117
+ If the disk cache is older than 30 days, the extension uses it immediately and runs a visible refresh on the next session start (progress appears in the UI widget). You can also run:
98
118
 
99
119
  ```
100
120
  /ollama-cloud-refresh
@@ -115,7 +135,7 @@ The plugin uses two Ollama Cloud API endpoints to build the model list:
115
135
 
116
136
  Only models with the `tools` capability are registered - these are the ones Pi can use for tool-calling.
117
137
 
118
- The raw `/api/show` responses are cached at `~/.pi/agent/cache/ollama-cloud-models.json` with a top-level `timestamp` value. If that local cache is older than 30 days, the plugin keeps using it immediately and triggers the visible refresh flow on `session_start`. If the cache is missing or invalid, the plugin registers a small hardcoded model list until refresh succeeds. If no key is available or refresh fails, the current registered list remains active until `/ollama-cloud-refresh` succeeds.
138
+ The raw `/api/show` responses are cached at `~/.pi/agent/cache/ollama-cloud-models.json` with a top-level `timestamp` value. If that local cache is older than 30 days, the plugin keeps using it immediately and runs a visible refresh on `session_start` (progress appears in the UI widget). If the cache is missing, the plugin uses the baked-in model list shipped with the extension (`models.generated.ts`).
119
139
 
120
140
  Model metadata is derived from the cached data:
121
141
 
@@ -163,6 +183,7 @@ Both tools use the same Ollama Cloud API key configured for the provider. No loc
163
183
  | Command | Description |
164
184
  |---|---|
165
185
  | `/ollama-cloud-refresh` | Fetch models from the Ollama Cloud API, update cache, and re-register the provider |
186
+ | `/ollama-webtools [on\|off\|enable\|disable]` | Enable or disable the `ollama_web_search` and `ollama_web_fetch` tools. Toggles if no argument given. |
166
187
 
167
188
  ## Development
168
189
 
@@ -220,5 +241,5 @@ Each publish also gets automatic [provenance attestation](https://docs.npmjs.com
220
241
 
221
242
  ## Notes
222
243
 
223
- - The extension sets `supportsDeveloperRole: false` on all models so the system prompt always uses `role: "system"`. Without this, pi sends the prompt as `role: "developer"` for thinking-capable models, which some models (e.g. GLM-5.1) ignore entirely - the prompt simply isn't read.
224
244
  - 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.
245
+ - `deepseek-v4` occasionally emits raw `<|DSML|tool_calls|>` markup as plain text instead of structured tool calls, then stops. This is DeepSeek's native tool-call format leaking through Ollama Cloud's OpenAI-compatible endpoint, so it looks like an upstream Ollama issue rather than something this extension can fix. If you hit it, retry or switch models.
package/config.ts ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Configuration loader for pi-ollama-cloud.
3
+ *
4
+ * Reads settings from JSON config files with project-over-global precedence:
5
+ * - ~/.pi/agent/ollama-cloud.json (global / user-level)
6
+ * - .pi/ollama-cloud.json (project-local, takes precedence)
7
+ *
8
+ * Environment variables serve as overrides above both config files:
9
+ * - PI_OLLAMA_WEB_TOOLS=0 disables web tool registration
10
+ *
11
+ * Example ollama-cloud.json:
12
+ * ```json
13
+ * {
14
+ * "webTools": false
15
+ * }
16
+ * ```
17
+ */
18
+
19
+ import { existsSync, readFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
22
+
23
+ // --- Types ---
24
+
25
+ export interface OllamaCloudConfig {
26
+ /** When false, ollama_web_search and ollama_web_fetch tools are not registered. Default: true. */
27
+ webTools?: boolean;
28
+ }
29
+
30
+ // --- Defaults ---
31
+
32
+ const DEFAULT_CONFIG: OllamaCloudConfig = {
33
+ webTools: true,
34
+ };
35
+
36
+ // --- Validation ---
37
+
38
+ /** Allowed config keys and their expected types for runtime validation. */
39
+ const CONFIG_SCHEMA: Record<keyof OllamaCloudConfig, "boolean"> = {
40
+ webTools: "boolean",
41
+ };
42
+
43
+ /**
44
+ * Validate a parsed JSON object against the known schema.
45
+ * Unknown keys are silently dropped; values with wrong types fall back to undefined.
46
+ */
47
+ function sanitizeConfig(raw: Record<string, unknown>): OllamaCloudConfig {
48
+ const out: OllamaCloudConfig = {};
49
+ for (const [key, expectedType] of Object.entries(CONFIG_SCHEMA)) {
50
+ const value = raw[key];
51
+ if (typeof value === expectedType) {
52
+ (out as Record<string, unknown>)[key] = value;
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ // --- Loader ---
59
+
60
+ /**
61
+ * Load configuration from JSON files.
62
+ * Project-local config overrides global config.
63
+ * Environment variables override both.
64
+ */
65
+ export function loadConfig(cwd: string): OllamaCloudConfig {
66
+ const globalPath = join(getAgentDir(), "ollama-cloud.json");
67
+ const projectPath = join(cwd, ".pi", "ollama-cloud.json");
68
+
69
+ let globalConfig: OllamaCloudConfig = {};
70
+ let projectConfig: OllamaCloudConfig = {};
71
+
72
+ // Load global config
73
+ if (existsSync(globalPath)) {
74
+ try {
75
+ const content = readFileSync(globalPath, "utf-8");
76
+ const parsed = JSON.parse(content);
77
+ // Silently skip files that parse to null, arrays, or primitives —
78
+ // malformed config should not crash the extension (defaults apply).
79
+ if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed)) {
80
+ globalConfig = sanitizeConfig(parsed as Record<string, unknown>);
81
+ }
82
+ } catch (err) {
83
+ console.error(`[pi-ollama-cloud] Failed to load config from ${globalPath}: ${err}`);
84
+ }
85
+ }
86
+
87
+ // Load project config
88
+ if (existsSync(projectPath)) {
89
+ try {
90
+ const content = readFileSync(projectPath, "utf-8");
91
+ const parsed = JSON.parse(content);
92
+ // Same guard as global config: null/array/primitive parses are ignored.
93
+ if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed)) {
94
+ projectConfig = sanitizeConfig(parsed as Record<string, unknown>);
95
+ }
96
+ } catch (err) {
97
+ console.error(`[pi-ollama-cloud] Failed to load config from ${projectPath}: ${err}`);
98
+ }
99
+ }
100
+
101
+ // Merge with defaults: defaults < global < project
102
+ const merged: OllamaCloudConfig = {
103
+ ...DEFAULT_CONFIG,
104
+ ...globalConfig,
105
+ ...projectConfig,
106
+ };
107
+
108
+ // Environment variable overrides (only webTools for now)
109
+ const envOverride = resolveWebToolsEnv();
110
+ if (envOverride !== undefined) {
111
+ merged.webTools = envOverride;
112
+ }
113
+
114
+ return merged;
115
+ }
116
+
117
+ /**
118
+ * Resolve the PI_OLLAMA_WEB_TOOLS environment variable override.
119
+ * Returns undefined when not set (no override),
120
+ * true/false when explicitly set.
121
+ */
122
+ export function resolveWebToolsEnv(): boolean | undefined {
123
+ const raw = process.env.PI_OLLAMA_WEB_TOOLS;
124
+ if (raw === undefined) return undefined;
125
+
126
+ const lowered = raw.toLowerCase();
127
+ if (["0", "false", "no", "off", ""].includes(lowered)) return false;
128
+ // Treat any other non-empty value as "enabled"
129
+ return true;
130
+ }
package/index.ts CHANGED
@@ -17,16 +17,21 @@
17
17
  * Raw /api/show responses are cached at <agentDir>/cache/ollama-cloud-models.json
18
18
  * so the provider assembly can be debugged and re-derived without re-fetching.
19
19
  *
20
- * Local cache entries include timestamp. Stale local caches are used immediately while a visible startup
21
- * refresh runs; missing/invalid caches use a small hardcoded model list until refresh completes.
20
+ * Startup behavior:
21
+ * - Missing cache: uses baked-in GENERATED_MODELS (manually generated via
22
+ * `npm run generate-models` and committed to the repo).
23
+ * - Stale cache (>30 days): uses the cached data immediately and triggers a visible refresh
24
+ * on session_start that shows progress in the UI widget.
25
+ * - Fresh cache: uses cached data directly, no refresh triggered.
22
26
  *
23
27
  * Only models with "tools" capability are registered.
24
28
  */
25
29
 
26
- import type { ExtensionAPI, ExtensionCommandContext, ProviderModelConfig } from "@mariozechner/pi-coding-agent";
30
+ import type { ExtensionAPI, ExtensionCommandContext, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
31
+ import { loadConfig, resolveWebToolsEnv } from "./config.ts";
32
+ import { GENERATED_MODELS } from "./models.generated.ts";
27
33
  import {
28
34
  assembleModels,
29
- FALLBACK_MODELS,
30
35
  fetchModels,
31
36
  OLLAMA_BASE,
32
37
  type RefreshProgress,
@@ -35,20 +40,11 @@ import {
35
40
  } from "./models.ts";
36
41
  import { registerWebFetchTool, registerWebSearchTool } from "./web-tools.ts";
37
42
 
38
- /**
39
- * Opt-out flag for the ollama_web_search and ollama_web_fetch tools.
40
- * When the value is one of "0", "false", "no", "off", or the empty string,
41
- * both web tool registrations are skipped. The model provider and
42
- * /ollama-cloud-refresh command remain active regardless.
43
- */
44
- const PI_OWT_RAW = process.env.PI_OLLAMA_WEB_TOOLS;
45
- const WEB_TOOLS_DISABLED =
46
- PI_OWT_RAW !== undefined && ["0", "false", "no", "off", ""].includes(PI_OWT_RAW.toLowerCase());
47
-
48
43
  // --- Registrations ---
49
44
 
50
45
  function registerProvider(pi: ExtensionAPI, models: ProviderModelConfig[]) {
51
46
  pi.registerProvider("ollama-cloud", {
47
+ name: "Ollama Cloud",
52
48
  baseUrl: `${OLLAMA_BASE}/v1`,
53
49
  apiKey: "OLLAMA_API_KEY",
54
50
  api: "openai-completions",
@@ -124,8 +120,14 @@ function registerRefreshCommand(pi: ExtensionAPI) {
124
120
 
125
121
  export default async function (pi: ExtensionAPI) {
126
122
  const cacheState = readCacheState();
127
- const needsStartupRefresh = cacheState.status !== "fresh";
128
- const models = cacheState.status === "missing" ? FALLBACK_MODELS : assembleModels(cacheState.models);
123
+ // Auto-refresh only when the disk cache is stale (>30 days).
124
+ // When cache is missing, GENERATED_MODELS serves as the cache —
125
+ // it is manually generated via `npm run generate-models` and committed to the repo.
126
+ const needsStartupRefresh = cacheState.status === "stale";
127
+ // GENERATED_MODELS ships with the package (36 tool-capable models from
128
+ // the build script). Used when no local cache exists. A fresh user cache
129
+ // from /ollama-cloud-refresh takes precedence over the generated list.
130
+ const models = cacheState.status === "missing" ? GENERATED_MODELS : assembleModels(cacheState.models);
129
131
 
130
132
  registerProvider(pi, models);
131
133
  registerRefreshCommand(pi);
@@ -139,8 +141,101 @@ export default async function (pi: ExtensionAPI) {
139
141
  });
140
142
  }
141
143
 
142
- if (!WEB_TOOLS_DISABLED) {
143
- registerWebSearchTool(pi);
144
- registerWebFetchTool(pi);
144
+ // --- Web Tools Management ---
145
+
146
+ /**
147
+ * Ensure web tools are registered (idempotent).
148
+ * Returns true if any tools were newly registered.
149
+ */
150
+ function ensureWebToolsRegistered(): boolean {
151
+ const allTools = pi.getAllTools();
152
+ let registered = false;
153
+ if (!allTools.some((t) => t.name === "ollama_web_search")) {
154
+ registerWebSearchTool(pi);
155
+ registered = true;
156
+ }
157
+ if (!allTools.some((t) => t.name === "ollama_web_fetch")) {
158
+ registerWebFetchTool(pi);
159
+ registered = true;
160
+ }
161
+ return registered;
162
+ }
163
+
164
+ /**
165
+ * Add or remove web tools from the active tools set.
166
+ */
167
+ function setWebToolsActive(active: boolean) {
168
+ const currentActive = pi.getActiveTools();
169
+ const webToolNames = ["ollama_web_search", "ollama_web_fetch"];
170
+
171
+ if (active) {
172
+ const missing = webToolNames.filter((n) => !currentActive.includes(n));
173
+ if (missing.length > 0) {
174
+ pi.setActiveTools([...currentActive, ...missing]);
175
+ }
176
+ } else {
177
+ const filtered = currentActive.filter((t) => !webToolNames.includes(t));
178
+ if (filtered.length < currentActive.length) {
179
+ pi.setActiveTools(filtered);
180
+ }
181
+ }
182
+ }
183
+
184
+ // Module-level tracking across session restarts within the same extension
185
+ // instance. The config file is read once, on the first session_start;
186
+ // later sessions reuse webToolsEnabled (including any /ollama-webtools
187
+ // override). Restart pi or /reload to pick up config file changes.
188
+ let webToolsConfigured = false;
189
+ let webToolsEnabled = false;
190
+
191
+ pi.on("session_start", async (_event, ctx) => {
192
+ if (!webToolsConfigured) {
193
+ webToolsConfigured = true;
194
+ const config = loadConfig(ctx.cwd);
195
+ if (config.webTools !== false) {
196
+ webToolsEnabled = true;
197
+ ensureWebToolsRegistered();
198
+ }
199
+ }
200
+ // On every session start (including resume/fork/new), re-apply the
201
+ // runtime state. Tools may have been unregistered during teardown.
202
+ if (webToolsEnabled) {
203
+ ensureWebToolsRegistered();
204
+ setWebToolsActive(true);
205
+ }
206
+ });
207
+
208
+ // Only register the runtime toggle command when the env var doesn't force tools off.
209
+ // PI_OLLAMA_WEB_TOOLS acts as a hard kill switch — no command to re-enable.
210
+ if (resolveWebToolsEnv() !== false) {
211
+ pi.registerCommand("ollama-webtools", {
212
+ description:
213
+ "Enable or disable Ollama Cloud web tools (ollama_web_search, ollama_web_fetch). " +
214
+ "Accepts optional argument: on/off/enable/disable. Without argument, toggles.",
215
+ handler: async (args, ctx) => {
216
+ const arg = args.trim().toLowerCase();
217
+
218
+ if (arg === "on" || arg === "enable") {
219
+ webToolsEnabled = true;
220
+ } else if (arg === "off" || arg === "disable") {
221
+ webToolsEnabled = false;
222
+ } else if (arg === "") {
223
+ // Toggle current state
224
+ webToolsEnabled = !webToolsEnabled;
225
+ } else {
226
+ ctx.ui.notify(`Unknown argument "${args.trim()}". Usage: /ollama-webtools [on|off|enable|disable]`, "error");
227
+ return;
228
+ }
229
+
230
+ if (webToolsEnabled) {
231
+ ensureWebToolsRegistered();
232
+ setWebToolsActive(true);
233
+ } else {
234
+ setWebToolsActive(false);
235
+ }
236
+
237
+ ctx.ui.notify(`Ollama Web Tools: ${webToolsEnabled ? "enabled" : "disabled"}`, "info");
238
+ },
239
+ });
145
240
  }
146
241
  }