pi-ollama-cloud 0.7.0 → 0.8.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 +12 -1
- package/README.md +58 -36
- package/index.ts +15 -113
- package/models.generated.ts +56 -56
- package/models.ts +162 -135
- package/package.json +7 -3
- package/pricing.generated.ts +6 -6
- package/web-tools.ts +65 -73
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [0.8.0] - 2026-08-09
|
|
6
|
+
|
|
7
|
+
- **Breaking:** Migrate model refresh to pi's native `refreshModels` mechanism. Remove the `/ollama-cloud-refresh` command and the manual `~/.pi/agent/cache/ollama-cloud-models.json` cache. The catalog now refreshes automatically on startup, on `/model` open, and via `pi update --models`, persisted through pi's own `FileModelsStore`. Users should delete the orphaned cache file after upgrade: `rm ~/.pi/agent/cache/ollama-cloud-models.json`.
|
|
8
|
+
- The shipped `GENERATED_MODELS` list is now a first-launch fallback; releases are no longer required for model freshness (only for deprecation or pricing changes).
|
|
9
|
+
- Refresh the generated catalog from the live API: add `deepseek-v4-flash:0731`, `deepseek-v4-flash:preview`, and `kimi-k3`; remove `kimi-k2.5` and `minimax-m2.5`, which are no longer listed. Refresh models.dev pricing, including the new models. Thanks @noruthedev (#43).
|
|
10
|
+
- Requires a pi version with the `stored`/`publish` `RefreshModelsContext` (pi 0.84.0+). Add `@earendil-works/pi-ai` as a peer dependency.
|
|
11
|
+
- Web tools now throw on errors instead of returning an `isError` result, aligning with pi 0.84.0's `AgentToolResult` contract.
|
|
12
|
+
- Fix `ollama_web_fetch` failing on pages where the API returns `links: null` (e.g. GitHub PR pages); the response is now accepted and rendered without a links list.
|
|
13
|
+
- Refresh robustness: add a 4-hour cooldown so repeated `/model` opens don't re-fetch the catalog; on a partial refresh failure, keep the last-good catalog and surface the error instead of silently returning an incomplete list.
|
|
14
|
+
- Add `tsgo --noEmit` type-checking to `npm run check` and CI.
|
|
15
|
+
|
|
16
|
+
## [0.7.0] - 2026-07-18
|
|
6
17
|
|
|
7
18
|
- Fix extension crash on pi 0.80.8+ where `AuthStorage` is no longer exported by `@earendil-works/pi-coding-agent`. Web tools now resolve the API key through the tool execution context's `modelRegistry.getApiKeyForProvider()`, preserving runtime/CLI overrides and the registered `apiKey: "$OLLAMA_API_KEY"` config. Thanks @badlogic for the cross-version analysis (#34, #35, #37).
|
|
8
19
|
- Restore the `OLLAMA_API_KEY` env-var fallback in `ollama_web_search` and `ollama_web_fetch`. Thanks @cawilliamson (#26).
|
package/README.md
CHANGED
|
@@ -8,10 +8,8 @@ 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
|
-
- **Baked-in model list** - A generated
|
|
12
|
-
- **
|
|
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.
|
|
14
|
-
- **`/ollama-cloud-refresh` command** - Re-fetches the model list and updates the cache and provider registration live (no restart needed).
|
|
11
|
+
- **Baked-in model list** - A generated fallback list (`models.generated.ts`) ships with the extension so models are available on first launch without any network calls. It is only a fallback: pi refreshes the live catalog at runtime, so shipping a new release for catalog freshness is no longer needed.
|
|
12
|
+
- **Automatic model refresh** - On startup, `/model` open, and `pi update --models`, pi calls the extension's `refreshModels` callback to fetch the latest models from the API and persists them through pi's own model store. No manual refresh command.
|
|
15
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.
|
|
16
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.
|
|
17
15
|
- **Estimated cost tracking** - Models are registered with estimated per-token costs sourced from [models.dev](https://models.dev) (the same catalog pi uses), so Pi's `/cost` shows comparable usage. Ollama Cloud is subscription-billed (Free, Pro, Max), so these are equivalent pay-as-you-go estimates, not actual charges. See [ollama.com/pricing](https://ollama.com/pricing) for plan details.
|
|
@@ -110,19 +108,7 @@ Example `ollama-cloud.json`:
|
|
|
110
108
|
|
|
111
109
|
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
110
|
|
|
113
|
-
### 4.
|
|
114
|
-
|
|
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.
|
|
116
|
-
|
|
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:
|
|
118
|
-
|
|
119
|
-
```
|
|
120
|
-
/ollama-cloud-refresh
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
This fetches the full model list from the Ollama Cloud API and overwrites the local cache.
|
|
124
|
-
|
|
125
|
-
### 5. Select a model
|
|
111
|
+
### 4. Select a model
|
|
126
112
|
|
|
127
113
|
Use `/model` or `Ctrl+L` to switch to an Ollama Cloud model. Models appear under the `ollama-cloud` provider.
|
|
128
114
|
|
|
@@ -135,9 +121,11 @@ The plugin uses two Ollama Cloud API endpoints to build the model list:
|
|
|
135
121
|
|
|
136
122
|
Only models with the `tools` capability are registered - these are the ones Pi can use for tool-calling.
|
|
137
123
|
|
|
138
|
-
The
|
|
124
|
+
The model list refreshes automatically: pi calls the extension's `refreshModels` callback on startup, when `/model` opens, and on `pi update --models`, fetching the live catalog and persisting it through pi's own model store. A model removed from the Ollama Cloud API disappears after the next successful refresh. The baked-in `models.generated.ts` list (regenerated via `npm run generate-models`) is only a first-launch fallback when no persisted catalog exists yet.
|
|
139
125
|
|
|
140
|
-
|
|
126
|
+
The model fetch itself is keyless (the `/v1/models` and `/api/show` endpoints are public), but pi only runs the live refresh when a credential resolves, so a user without a configured API key stays on the baked-in list until they add one. That is a non-issue in practice because a credentialless user cannot run models anyway.
|
|
127
|
+
|
|
128
|
+
Model metadata is derived from the `/api/show` response:
|
|
141
129
|
|
|
142
130
|
| Field | Source |
|
|
143
131
|
|---|---|
|
|
@@ -146,7 +134,7 @@ Model metadata is derived from the cached data:
|
|
|
146
134
|
| `input` | `["text", "image"]` if `capabilities` includes `"vision"`, else `["text"]` |
|
|
147
135
|
| `contextWindow` | `model_info.*.context_length` (falls back to 128000) |
|
|
148
136
|
| `maxTokens` | Fixed at 32768 |
|
|
149
|
-
| `cost` | Estimated per-1M-token prices from [models.dev](https://models.dev), generated by `scripts/generate-pricing.ts` into `pricing.generated.ts`. Ollama Cloud is subscription-billed, so these are equivalent pay-as-you-go estimates, not actual charges. Unmapped models default to zero. Prices are pinned to the installed package version
|
|
137
|
+
| `cost` | Estimated per-1M-token prices from [models.dev](https://models.dev), generated by `scripts/generate-pricing.ts` into `pricing.generated.ts`. Ollama Cloud is subscription-billed, so these are equivalent pay-as-you-go estimates, not actual charges. Unmapped models default to zero. Prices are pinned to the installed package version and only update on a new release, so newly added models register with zero cost until then. |
|
|
150
138
|
|
|
151
139
|
### Thinking level mapping
|
|
152
140
|
|
|
@@ -162,14 +150,6 @@ Pi's thinking levels are mapped to Ollama Cloud's OpenAI-compatible `reasoning_e
|
|
|
162
150
|
|
|
163
151
|
See [docs/think-experiment.md](docs/think-experiment.md) for the testing methodology and results.
|
|
164
152
|
|
|
165
|
-
Refresh from inside Pi:
|
|
166
|
-
|
|
167
|
-
```text
|
|
168
|
-
/ollama-cloud-refresh
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
That command updates `~/.pi/agent/cache/ollama-cloud-models.json` with a new `timestamp` and re-registers the provider live, so no restart is required.
|
|
172
|
-
|
|
173
153
|
## Tools
|
|
174
154
|
|
|
175
155
|
| Tool | Description |
|
|
@@ -183,19 +163,48 @@ Both tools use the same Ollama Cloud API key configured for the provider. No loc
|
|
|
183
163
|
|
|
184
164
|
| Command | Description |
|
|
185
165
|
|---|---|
|
|
186
|
-
| `/ollama-cloud-refresh` | Fetch models from the Ollama Cloud API, update cache, and re-register the provider. Updates the model list and metadata but not estimated prices (those are pinned to the installed package version). |
|
|
187
166
|
| `/ollama-webtools [on\|off\|enable\|disable]` | Enable or disable the `ollama_web_search` and `ollama_web_fetch` tools. Toggles if no argument given. |
|
|
188
167
|
|
|
189
168
|
## Development
|
|
190
169
|
|
|
191
170
|
```bash
|
|
192
|
-
npm install # install devDependencies
|
|
193
|
-
npm run check # lint + format
|
|
194
|
-
npm run lint
|
|
195
|
-
npm run
|
|
171
|
+
npm install # install devDependencies
|
|
172
|
+
npm run check # lint + format + type-check (auto-fix)
|
|
173
|
+
npm run lint # lint only (no fixes)
|
|
174
|
+
npm run typecheck # type-check only (tsgo --noEmit)
|
|
175
|
+
npm run format # format only
|
|
196
176
|
```
|
|
197
177
|
|
|
198
|
-
The project uses [Biome](https://biomejs.dev/) for linting and formatting (2-space indent, line width 120).
|
|
178
|
+
The project uses [Biome](https://biomejs.dev/) for linting and formatting (2-space indent, line width 120) and [tsgo](https://github.com/microsoft/typescript-go) for type-checking.
|
|
179
|
+
|
|
180
|
+
### Testing local changes
|
|
181
|
+
|
|
182
|
+
Static checks (no API key needed):
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm install
|
|
186
|
+
npm run check # lint + format + type-check
|
|
187
|
+
npm run test # unit tests
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Live smoke against the real API (needs an `OLLAMA_API_KEY` or an `ollama-cloud` entry in `auth.json`):
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Run pi with the local extension, no install required. The --no-* flags isolate
|
|
194
|
+
# the run from other installed extensions, skills, prompt templates, themes,
|
|
195
|
+
# context files, and session storage so only the local checkout is exercised.
|
|
196
|
+
pi --no-extensions --no-skills --no-prompt-templates --no-themes --no-context-files --no-session \
|
|
197
|
+
-e ./index.ts --model "ollama-cloud/gemma4:31b" --no-tools -p "Say hi in one word"
|
|
198
|
+
|
|
199
|
+
# Verify thinking is suppressed when off
|
|
200
|
+
pi --no-extensions --no-skills --no-prompt-templates --no-themes --no-context-files --no-session \
|
|
201
|
+
-e ./index.ts --thinking off --model "ollama-cloud/glm-5.2" --no-tools --mode json -p 'hi'
|
|
202
|
+
|
|
203
|
+
# Web tools
|
|
204
|
+
npm run smoke:web-tools
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The `-e`/`--extension` flag loads the extension from the local checkout without installing it; `--no-extensions` disables all other extension discovery so the run cannot pick up an installed `pi-ollama-cloud` or other plugins. The same commands run in CI (`.github/workflows/test.yml`), gated on the `OLLAMA_CLOUD_API_KEY` secret.
|
|
199
208
|
|
|
200
209
|
## How is this different from `ollama launch pi`?
|
|
201
210
|
|
|
@@ -211,7 +220,7 @@ The project uses [Biome](https://biomejs.dev/) for linting and formatting (2-spa
|
|
|
211
220
|
| **Authentication** | Handled by the local server (sign-in flow via `ollama`) | Ollama Cloud API key (set via `OLLAMA_API_KEY` or `auth.json`) |
|
|
212
221
|
| **Model discovery** | Interactive picker with curated recommendations + pulled models | Dynamic - fetches all available cloud models with tool support from the API |
|
|
213
222
|
| **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) |
|
|
214
|
-
| **Setup effort** | One command: `ollama launch pi` | Install extension + API key
|
|
223
|
+
| **Setup effort** | One command: `ollama launch pi` | Install extension + API key |
|
|
215
224
|
| **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 |
|
|
216
225
|
|
|
217
226
|
**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.
|
|
@@ -229,6 +238,11 @@ npm version minor # or patch, or major
|
|
|
229
238
|
git push --tags
|
|
230
239
|
```
|
|
231
240
|
|
|
241
|
+
Because the model catalog refreshes automatically at runtime, a release is **not** needed to ship new models. Publish only when:
|
|
242
|
+
|
|
243
|
+
- A model is retired and still listed by the API: add it to `RETIRED_MODEL_IDS` in `scripts/generate-models.ts` (check https://docs.ollama.com/cloud#retirements, then regenerate `models.generated.ts`).
|
|
244
|
+
- Pricing changes: models.dev prices updated, or a new model needs an `OLLAMA_TO_MODELSDEV` mapping line (regenerate `pricing.generated.ts`).
|
|
245
|
+
|
|
232
246
|
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.
|
|
233
247
|
|
|
234
248
|
The workflow uses npm's [trusted publishing](https://docs.npmjs.com/trusted-publishers/) (OIDC) - no tokens stored as secrets. To set it up:
|
|
@@ -240,7 +254,15 @@ The workflow uses npm's [trusted publishing](https://docs.npmjs.com/trusted-publ
|
|
|
240
254
|
|
|
241
255
|
Each publish also gets automatic [provenance attestation](https://docs.npmjs.com/generating-provenance-statements).
|
|
242
256
|
|
|
257
|
+
## Upgrading
|
|
258
|
+
|
|
259
|
+
Since 0.8.0:
|
|
260
|
+
|
|
261
|
+
- The `/ollama-cloud-refresh` command is removed. Models refresh automatically on startup, `/model` open, and `pi update --models`.
|
|
262
|
+
- The old cache file at `~/.pi/agent/cache/ollama-cloud-models.json` is orphaned. Delete it manually: `rm ~/.pi/agent/cache/ollama-cloud-models.json`.
|
|
263
|
+
- Requires a pi version with the native `refreshModels` API (pi 0.84.0+).
|
|
264
|
+
|
|
243
265
|
## Notes
|
|
244
266
|
|
|
245
|
-
- The fetch timeout is 10 seconds per request. On slow connections, some model detail fetches may time out
|
|
267
|
+
- The fetch timeout is 10 seconds per request. On slow connections, some model detail fetches may time out; the refresh uses whatever succeeded and only fails if every model detail fetch fails.
|
|
246
268
|
- `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/index.ts
CHANGED
|
@@ -1,144 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ollama Cloud Provider Extension
|
|
3
3
|
*
|
|
4
|
-
* Registers Ollama Cloud as a model provider with
|
|
4
|
+
* Registers Ollama Cloud as a model provider with a baked-in fallback catalog
|
|
5
|
+
* and a native `refreshModels` callback that overlays live API updates.
|
|
5
6
|
*
|
|
6
7
|
* Setup:
|
|
7
8
|
* 1. Get an API key from https://ollama.com
|
|
8
9
|
* 2. Add to auth.json in the agent config dir (~/.pi/agent/auth.json, or set PI_CODING_AGENT_DIR):
|
|
9
10
|
* { "ollama-cloud": { "type": "api_key", "key": "your-key" } }
|
|
10
|
-
* 3.
|
|
11
|
-
* 4. Use /model or ctrl+l to select an Ollama Cloud model
|
|
11
|
+
* 3. Use /model or ctrl+l to select an Ollama Cloud model
|
|
12
12
|
*
|
|
13
13
|
* Two endpoints are used to build the model list:
|
|
14
14
|
* - GET https://ollama.com/v1/models -> list of model IDs
|
|
15
15
|
* - POST https://ollama.com/api/show -> per-model details (capabilities, context length)
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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.
|
|
17
|
+
* Catalog behavior:
|
|
18
|
+
* - The baked-in GENERATED_MODELS list (via `npm run generate-models`) is the
|
|
19
|
+
* first-launch fallback when no persisted catalog exists.
|
|
20
|
+
* - On startup, /model open, and `pi update --models`, pi calls the
|
|
21
|
+
* `refreshModels` callback, which fetches the live catalog and persists it
|
|
22
|
+
* through pi's own FileModelsStore. Refresh is automatic.
|
|
26
23
|
*
|
|
27
24
|
* Only models with "tools" capability are registered.
|
|
28
25
|
*/
|
|
29
26
|
|
|
30
|
-
import type { ExtensionAPI
|
|
27
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
31
28
|
import { loadConfig, resolveWebToolsEnv } from "./config.ts";
|
|
32
29
|
import { GENERATED_MODELS } from "./models.generated.ts";
|
|
33
|
-
import {
|
|
34
|
-
assembleModels,
|
|
35
|
-
fetchModels,
|
|
36
|
-
OLLAMA_BASE,
|
|
37
|
-
type RefreshProgress,
|
|
38
|
-
readCacheState,
|
|
39
|
-
writeCache,
|
|
40
|
-
} from "./models.ts";
|
|
30
|
+
import { OLLAMA_BASE, refreshOllamaCatalog } from "./models.ts";
|
|
41
31
|
import { registerWebFetchTool, registerWebSearchTool } from "./web-tools.ts";
|
|
42
32
|
|
|
43
|
-
// ---
|
|
33
|
+
// --- Main ---
|
|
44
34
|
|
|
45
|
-
function
|
|
35
|
+
export default async function (pi: ExtensionAPI) {
|
|
46
36
|
pi.registerProvider("ollama-cloud", {
|
|
47
37
|
name: "Ollama Cloud",
|
|
48
38
|
baseUrl: `${OLLAMA_BASE}/v1`,
|
|
49
39
|
apiKey: "$OLLAMA_API_KEY",
|
|
50
40
|
api: "openai-completions",
|
|
51
|
-
models,
|
|
41
|
+
models: GENERATED_MODELS,
|
|
42
|
+
refreshModels: refreshOllamaCatalog,
|
|
52
43
|
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function renderProgressBar(current: number, total: number, width = 15): string {
|
|
56
|
-
if (total <= 0) return `[${"░".repeat(width)}]`;
|
|
57
|
-
const ratio = Math.max(0, Math.min(1, current / total));
|
|
58
|
-
const filled = Math.round(ratio * width);
|
|
59
|
-
return `[${"█".repeat(filled)}${"░".repeat(width - filled)}]`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function createRefreshProgressUi(ctx: Pick<ExtensionCommandContext, "ui">) {
|
|
63
|
-
const key = "ollama-cloud-refresh";
|
|
64
|
-
return {
|
|
65
|
-
update(progress: RefreshProgress) {
|
|
66
|
-
const current = progress.current ?? 0;
|
|
67
|
-
const total = progress.total ?? 0;
|
|
68
|
-
const percent = total > 0 ? Math.round((current / total) * 100) : 0;
|
|
69
|
-
const failed = progress.failed ? `, ${progress.failed} failed` : "";
|
|
70
|
-
const stage =
|
|
71
|
-
progress.stage === "list"
|
|
72
|
-
? "Discovering models"
|
|
73
|
-
: progress.stage === "details"
|
|
74
|
-
? "Fetching model details"
|
|
75
|
-
: "Done";
|
|
76
|
-
const summary = total > 0 ? `${current}/${total} (${percent}%${failed})` : progress.message;
|
|
77
|
-
const line = `☁ Ollama Cloud - ${stage} — ${summary} ${renderProgressBar(current, total)}`;
|
|
78
|
-
|
|
79
|
-
ctx.ui.setWorkingMessage(`Refreshing Ollama Cloud models - ${stage.toLowerCase()}`);
|
|
80
|
-
ctx.ui.setWidget(key, [line], { placement: "belowEditor" });
|
|
81
|
-
},
|
|
82
|
-
clear() {
|
|
83
|
-
ctx.ui.setWidget(key, undefined);
|
|
84
|
-
ctx.ui.setStatus(key, undefined);
|
|
85
|
-
ctx.ui.setWorkingMessage();
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function runRefresh(pi: ExtensionAPI, ctx: Pick<ExtensionCommandContext, "ui">) {
|
|
91
|
-
const progressUi = createRefreshProgressUi(ctx);
|
|
92
|
-
try {
|
|
93
|
-
progressUi.update({ stage: "list", message: "Starting refresh..." });
|
|
94
|
-
|
|
95
|
-
const raw = await fetchModels(ctx, (progress) => progressUi.update(progress));
|
|
96
|
-
if (!raw) return false;
|
|
97
|
-
|
|
98
|
-
writeCache(raw);
|
|
99
|
-
const newModels = assembleModels(raw);
|
|
100
|
-
|
|
101
|
-
registerProvider(pi, newModels);
|
|
102
|
-
|
|
103
|
-
ctx.ui.notify(`Registered ${newModels.length} Ollama Cloud models`, "info");
|
|
104
|
-
return true;
|
|
105
|
-
} finally {
|
|
106
|
-
progressUi.clear();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function registerRefreshCommand(pi: ExtensionAPI) {
|
|
111
|
-
pi.registerCommand("ollama-cloud-refresh", {
|
|
112
|
-
description: "Refresh Ollama Cloud models from the API",
|
|
113
|
-
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
114
|
-
await runRefresh(pi, ctx);
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// --- Main ---
|
|
120
|
-
|
|
121
|
-
export default async function (pi: ExtensionAPI) {
|
|
122
|
-
const cacheState = readCacheState();
|
|
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. Used when no local cache exists. A
|
|
128
|
-
// fresh user cache from /ollama-cloud-refresh takes precedence over the generated list.
|
|
129
|
-
const models = cacheState.status === "missing" ? GENERATED_MODELS : assembleModels(cacheState.models);
|
|
130
|
-
|
|
131
|
-
registerProvider(pi, models);
|
|
132
|
-
registerRefreshCommand(pi);
|
|
133
|
-
|
|
134
|
-
if (needsStartupRefresh) {
|
|
135
|
-
let started = false;
|
|
136
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
137
|
-
if (started) return;
|
|
138
|
-
started = true;
|
|
139
|
-
await runRefresh(pi, ctx);
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
44
|
|
|
143
45
|
// --- Web Tools Management ---
|
|
144
46
|
|
package/models.generated.ts
CHANGED
|
@@ -1,14 +1,54 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-models.ts
|
|
2
2
|
// Do not edit manually.
|
|
3
|
-
// Generated: 2026-
|
|
3
|
+
// Generated: 2026-08-08T04:19:46.959Z
|
|
4
4
|
// Model count: 18
|
|
5
5
|
|
|
6
6
|
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
|
|
8
8
|
export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
9
9
|
{
|
|
10
|
-
id: "deepseek-v4-flash",
|
|
11
|
-
name: "deepseek-v4-flash",
|
|
10
|
+
id: "deepseek-v4-flash:0731",
|
|
11
|
+
name: "deepseek-v4-flash:0731",
|
|
12
|
+
compat: {
|
|
13
|
+
maxTokensField: "max_tokens",
|
|
14
|
+
openRouterRouting: {},
|
|
15
|
+
requiresAssistantAfterToolResult: false,
|
|
16
|
+
requiresReasoningContentOnAssistantMessages: false,
|
|
17
|
+
requiresThinkingAsText: false,
|
|
18
|
+
requiresToolResultName: false,
|
|
19
|
+
sendSessionAffinityHeaders: false,
|
|
20
|
+
supportsDeveloperRole: false,
|
|
21
|
+
supportsLongCacheRetention: false,
|
|
22
|
+
supportsReasoningEffort: true,
|
|
23
|
+
supportsStore: false,
|
|
24
|
+
supportsStrictMode: false,
|
|
25
|
+
supportsUsageInStreaming: true,
|
|
26
|
+
thinkingFormat: "openai",
|
|
27
|
+
vercelGatewayRouting: {},
|
|
28
|
+
zaiToolStream: false,
|
|
29
|
+
},
|
|
30
|
+
contextWindow: 1048576,
|
|
31
|
+
cost: {
|
|
32
|
+
cacheRead: 0.0028,
|
|
33
|
+
cacheWrite: 0,
|
|
34
|
+
input: 0.14,
|
|
35
|
+
output: 0.28,
|
|
36
|
+
},
|
|
37
|
+
input: ["text"],
|
|
38
|
+
maxTokens: 32768,
|
|
39
|
+
reasoning: true,
|
|
40
|
+
thinkingLevelMap: {
|
|
41
|
+
high: "high",
|
|
42
|
+
low: "low",
|
|
43
|
+
medium: "medium",
|
|
44
|
+
minimal: null,
|
|
45
|
+
off: "none",
|
|
46
|
+
xhigh: "max",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "deepseek-v4-flash:preview",
|
|
51
|
+
name: "deepseek-v4-flash:preview",
|
|
12
52
|
compat: {
|
|
13
53
|
maxTokensField: "max_tokens",
|
|
14
54
|
openRouterRouting: {},
|
|
@@ -109,10 +149,10 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
109
149
|
},
|
|
110
150
|
contextWindow: 262144,
|
|
111
151
|
cost: {
|
|
112
|
-
cacheRead: 0.
|
|
152
|
+
cacheRead: 0.1,
|
|
113
153
|
cacheWrite: 0,
|
|
114
|
-
input: 0.
|
|
115
|
-
output: 0.
|
|
154
|
+
input: 0.1,
|
|
155
|
+
output: 0.34,
|
|
116
156
|
},
|
|
117
157
|
input: ["text", "image"],
|
|
118
158
|
maxTokens: 32768,
|
|
@@ -286,46 +326,6 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
286
326
|
xhigh: null,
|
|
287
327
|
},
|
|
288
328
|
},
|
|
289
|
-
{
|
|
290
|
-
id: "kimi-k2.5",
|
|
291
|
-
name: "kimi-k2.5",
|
|
292
|
-
compat: {
|
|
293
|
-
maxTokensField: "max_tokens",
|
|
294
|
-
openRouterRouting: {},
|
|
295
|
-
requiresAssistantAfterToolResult: false,
|
|
296
|
-
requiresReasoningContentOnAssistantMessages: false,
|
|
297
|
-
requiresThinkingAsText: false,
|
|
298
|
-
requiresToolResultName: false,
|
|
299
|
-
sendSessionAffinityHeaders: false,
|
|
300
|
-
supportsDeveloperRole: false,
|
|
301
|
-
supportsLongCacheRetention: false,
|
|
302
|
-
supportsReasoningEffort: true,
|
|
303
|
-
supportsStore: false,
|
|
304
|
-
supportsStrictMode: false,
|
|
305
|
-
supportsUsageInStreaming: true,
|
|
306
|
-
thinkingFormat: "openai",
|
|
307
|
-
vercelGatewayRouting: {},
|
|
308
|
-
zaiToolStream: false,
|
|
309
|
-
},
|
|
310
|
-
contextWindow: 262144,
|
|
311
|
-
cost: {
|
|
312
|
-
cacheRead: 0.1,
|
|
313
|
-
cacheWrite: 0,
|
|
314
|
-
input: 0.6,
|
|
315
|
-
output: 3,
|
|
316
|
-
},
|
|
317
|
-
input: ["text", "image"],
|
|
318
|
-
maxTokens: 32768,
|
|
319
|
-
reasoning: true,
|
|
320
|
-
thinkingLevelMap: {
|
|
321
|
-
high: "high",
|
|
322
|
-
low: "low",
|
|
323
|
-
medium: "medium",
|
|
324
|
-
minimal: null,
|
|
325
|
-
off: "none",
|
|
326
|
-
xhigh: "max",
|
|
327
|
-
},
|
|
328
|
-
},
|
|
329
329
|
{
|
|
330
330
|
id: "kimi-k2.6",
|
|
331
331
|
name: "kimi-k2.6",
|
|
@@ -407,8 +407,8 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
407
407
|
},
|
|
408
408
|
},
|
|
409
409
|
{
|
|
410
|
-
id: "
|
|
411
|
-
name: "
|
|
410
|
+
id: "kimi-k3",
|
|
411
|
+
name: "kimi-k3",
|
|
412
412
|
compat: {
|
|
413
413
|
maxTokensField: "max_tokens",
|
|
414
414
|
openRouterRouting: {},
|
|
@@ -427,14 +427,14 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
427
427
|
vercelGatewayRouting: {},
|
|
428
428
|
zaiToolStream: false,
|
|
429
429
|
},
|
|
430
|
-
contextWindow:
|
|
430
|
+
contextWindow: 1048576,
|
|
431
431
|
cost: {
|
|
432
|
-
cacheRead: 0.
|
|
433
|
-
cacheWrite: 0
|
|
434
|
-
input:
|
|
435
|
-
output:
|
|
432
|
+
cacheRead: 0.3,
|
|
433
|
+
cacheWrite: 0,
|
|
434
|
+
input: 3,
|
|
435
|
+
output: 15,
|
|
436
436
|
},
|
|
437
|
-
input: ["text"],
|
|
437
|
+
input: ["text", "image"],
|
|
438
438
|
maxTokens: 32768,
|
|
439
439
|
reasoning: true,
|
|
440
440
|
thinkingLevelMap: {
|
|
@@ -442,7 +442,7 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
442
442
|
low: "low",
|
|
443
443
|
medium: "medium",
|
|
444
444
|
minimal: null,
|
|
445
|
-
off:
|
|
445
|
+
off: "none",
|
|
446
446
|
xhigh: "max",
|
|
447
447
|
},
|
|
448
448
|
},
|
|
@@ -581,7 +581,7 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
581
581
|
},
|
|
582
582
|
contextWindow: 262144,
|
|
583
583
|
cost: {
|
|
584
|
-
cacheRead: 0,
|
|
584
|
+
cacheRead: 0.03,
|
|
585
585
|
cacheWrite: 0,
|
|
586
586
|
input: 0.05,
|
|
587
587
|
output: 0.2,
|
package/models.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type { RefreshModelsContext } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { GENERATED_MODELS } from "./models.generated.ts";
|
|
4
4
|
import { MODEL_PRICING, type ModelPrice } from "./pricing.generated.ts";
|
|
5
5
|
import { resolve as resolveThinkingLevelMap } from "./thinking-levels.ts";
|
|
6
6
|
import { concurrentMap, fetchJsonWithTimeout, getContextLength } from "./utils.ts";
|
|
@@ -18,10 +18,10 @@ function resolvePrice(id: string): ModelPrice {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// --- Constants ---
|
|
21
|
-
const CACHE_DIR = join(getAgentDir(), "cache");
|
|
22
|
-
const CACHE_FILE = join(CACHE_DIR, "ollama-cloud-models.json");
|
|
23
|
-
const CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
24
21
|
const FETCH_TIMEOUT_MS = 10000;
|
|
22
|
+
// How long a stored catalog is considered fresh before the next network refresh
|
|
23
|
+
// (mirrors pi-mono's REMOTE_CATALOG_REFRESH_INTERVAL_MS in remote-catalog-provider.ts).
|
|
24
|
+
const REFRESH_COOLDOWN_MS = 4 * 60 * 60 * 1000;
|
|
25
25
|
|
|
26
26
|
// The cloud extension always targets ollama.com; local Ollama daemons (typically
|
|
27
27
|
// pointed at via OLLAMA_API_BASE for the local CLI) are a different product and
|
|
@@ -67,25 +67,6 @@ interface OllamaShowResponse {
|
|
|
67
67
|
modified_at: string;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
type CachedOllamaModel = OllamaShowResponse;
|
|
71
|
-
|
|
72
|
-
/** On-disk cache: raw /api/show responses keyed by model ID. */
|
|
73
|
-
interface CachedData {
|
|
74
|
-
/** Unix epoch milliseconds used to decide when the generated metadata is stale. */
|
|
75
|
-
timestamp?: number;
|
|
76
|
-
models: Record<string, CachedOllamaModel>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
type RefreshProgressStage = "list" | "details" | "done";
|
|
80
|
-
|
|
81
|
-
export interface RefreshProgress {
|
|
82
|
-
stage: RefreshProgressStage;
|
|
83
|
-
current?: number;
|
|
84
|
-
total?: number;
|
|
85
|
-
failed?: number;
|
|
86
|
-
message: string;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
70
|
// --- Assembly: raw API data -> ProviderModelConfig[] ---
|
|
90
71
|
|
|
91
72
|
/**
|
|
@@ -135,7 +116,7 @@ function buildCompat(): ProviderModelConfig["compat"] {
|
|
|
135
116
|
};
|
|
136
117
|
}
|
|
137
118
|
|
|
138
|
-
export function assembleModels(raw: Record<string,
|
|
119
|
+
export function assembleModels(raw: Record<string, OllamaShowResponse>): ProviderModelConfig[] {
|
|
139
120
|
return Object.entries(raw)
|
|
140
121
|
.filter(([, data]) => data.capabilities?.includes("tools"))
|
|
141
122
|
.map(([id, data]) => ({
|
|
@@ -153,60 +134,8 @@ export function assembleModels(raw: Record<string, CachedOllamaModel>): Provider
|
|
|
153
134
|
}));
|
|
154
135
|
}
|
|
155
136
|
|
|
156
|
-
// --- Cache I/O ---
|
|
157
|
-
type CacheState =
|
|
158
|
-
| { status: "fresh"; models: Record<string, CachedOllamaModel> }
|
|
159
|
-
| { status: "stale"; models: Record<string, CachedOllamaModel> }
|
|
160
|
-
| { status: "missing" };
|
|
161
|
-
|
|
162
|
-
function createCacheData(models: Record<string, CachedOllamaModel>, now = new Date()): CachedData {
|
|
163
|
-
return { timestamp: now.getTime(), models };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function readCacheData(path: string): CachedData | null {
|
|
167
|
-
try {
|
|
168
|
-
const data: CachedData = JSON.parse(readFileSync(path, "utf-8"));
|
|
169
|
-
if (!data.models || Object.keys(data.models).length === 0) return null;
|
|
170
|
-
return data;
|
|
171
|
-
} catch {
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function isFreshGeneratedCache(data: CachedData): boolean {
|
|
177
|
-
if (typeof data.timestamp !== "number" || !Number.isFinite(data.timestamp)) return false;
|
|
178
|
-
return Date.now() - data.timestamp <= CACHE_MAX_AGE_MS;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function readCacheState(): CacheState {
|
|
182
|
-
if (!existsSync(CACHE_FILE)) return { status: "missing" };
|
|
183
|
-
|
|
184
|
-
const data = readCacheData(CACHE_FILE);
|
|
185
|
-
if (!data) {
|
|
186
|
-
try {
|
|
187
|
-
rmSync(CACHE_FILE, { force: true });
|
|
188
|
-
} catch {
|
|
189
|
-
// Ignore cache delete errors.
|
|
190
|
-
}
|
|
191
|
-
return { status: "missing" };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return isFreshGeneratedCache(data)
|
|
195
|
-
? { status: "fresh", models: data.models }
|
|
196
|
-
: { status: "stale", models: data.models };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export function writeCache(models: Record<string, CachedOllamaModel>): void {
|
|
200
|
-
try {
|
|
201
|
-
mkdirSync(CACHE_DIR, { recursive: true });
|
|
202
|
-
writeFileSync(CACHE_FILE, JSON.stringify(createCacheData(models), null, 2));
|
|
203
|
-
} catch {
|
|
204
|
-
// Ignore cache write errors
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
137
|
// --- Fetch Models ---
|
|
209
|
-
export async function fetchModelIds(timeoutMs = FETCH_TIMEOUT_MS): Promise<string[]> {
|
|
138
|
+
export async function fetchModelIds(signal?: AbortSignal, timeoutMs = FETCH_TIMEOUT_MS): Promise<string[]> {
|
|
210
139
|
const headers: Record<string, string> = {};
|
|
211
140
|
const apiKey = process.env.OLLAMA_API_KEY;
|
|
212
141
|
if (apiKey) {
|
|
@@ -217,10 +146,11 @@ export async function fetchModelIds(timeoutMs = FETCH_TIMEOUT_MS): Promise<strin
|
|
|
217
146
|
`${OLLAMA_BASE}/v1/models`,
|
|
218
147
|
{ headers },
|
|
219
148
|
timeoutMs,
|
|
149
|
+
signal,
|
|
220
150
|
);
|
|
221
151
|
|
|
222
152
|
if (res.status === 429) {
|
|
223
|
-
throw new Error("Ollama Cloud rate limited. Try again shortly.");
|
|
153
|
+
throw new Error("Ollama Cloud model list fetch rate limited. Try again shortly.");
|
|
224
154
|
}
|
|
225
155
|
if (!res.ok || !res.data) {
|
|
226
156
|
throw new Error(`Failed to fetch model list: ${res.status}${res.error ? ` - ${res.error}` : ""}`);
|
|
@@ -229,7 +159,11 @@ export async function fetchModelIds(timeoutMs = FETCH_TIMEOUT_MS): Promise<strin
|
|
|
229
159
|
return res.data.data.map((m) => m.id);
|
|
230
160
|
}
|
|
231
161
|
|
|
232
|
-
export async function fetchModelDetails(
|
|
162
|
+
export async function fetchModelDetails(
|
|
163
|
+
id: string,
|
|
164
|
+
signal?: AbortSignal,
|
|
165
|
+
timeoutMs = FETCH_TIMEOUT_MS,
|
|
166
|
+
): Promise<OllamaShowResponse> {
|
|
233
167
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
234
168
|
const apiKey = process.env.OLLAMA_API_KEY;
|
|
235
169
|
if (apiKey) {
|
|
@@ -244,10 +178,11 @@ export async function fetchModelDetails(id: string, timeoutMs = FETCH_TIMEOUT_MS
|
|
|
244
178
|
body: JSON.stringify({ model: id }),
|
|
245
179
|
},
|
|
246
180
|
timeoutMs,
|
|
181
|
+
signal,
|
|
247
182
|
);
|
|
248
183
|
|
|
249
184
|
if (res.status === 429) {
|
|
250
|
-
throw new Error("Ollama Cloud rate limited. Try again shortly.");
|
|
185
|
+
throw new Error("Ollama Cloud /api/show rate limited. Try again shortly.");
|
|
251
186
|
}
|
|
252
187
|
if (!res.ok || !res.data) {
|
|
253
188
|
throw new Error(`Failed to fetch /api/show for ${id}: ${res.status}${res.error ? ` - ${res.error}` : ""}`);
|
|
@@ -256,69 +191,161 @@ export async function fetchModelDetails(id: string, timeoutMs = FETCH_TIMEOUT_MS
|
|
|
256
191
|
return res.data;
|
|
257
192
|
}
|
|
258
193
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
let detailsDone = 0;
|
|
272
|
-
let detailsFailed = 0;
|
|
273
|
-
const detailResults = await concurrentMap(modelIds, params.workers ?? 8, async (id) => {
|
|
274
|
-
try {
|
|
275
|
-
return [id, await fetchModelDetails(id)] as const;
|
|
276
|
-
} catch (error) {
|
|
277
|
-
detailsFailed++;
|
|
278
|
-
throw error;
|
|
279
|
-
} finally {
|
|
280
|
-
detailsDone++;
|
|
281
|
-
onProgress({
|
|
282
|
-
stage: "details",
|
|
283
|
-
current: detailsDone,
|
|
284
|
-
total: modelIds.length,
|
|
285
|
-
failed: detailsFailed,
|
|
286
|
-
message: "Fetching model details",
|
|
287
|
-
});
|
|
288
|
-
}
|
|
194
|
+
/**
|
|
195
|
+
* Fetch per-model /api/show details for a list of model IDs, 8 workers at a time.
|
|
196
|
+
* Returns the models that succeeded. Throws when every detail request fails
|
|
197
|
+
* (the zero-succeeded case), so the caller can surface a real failure instead
|
|
198
|
+
* of an empty catalog.
|
|
199
|
+
*/
|
|
200
|
+
export async function refreshOllamaCloudModels(
|
|
201
|
+
modelIds: string[],
|
|
202
|
+
signal?: AbortSignal,
|
|
203
|
+
): Promise<{ models: Record<string, OllamaShowResponse>; failed: number }> {
|
|
204
|
+
const detailResults = await concurrentMap(modelIds, 8, async (id) => {
|
|
205
|
+
return [id, await fetchModelDetails(id, signal)] as const;
|
|
289
206
|
});
|
|
290
|
-
const models: Record<string,
|
|
207
|
+
const models: Record<string, OllamaShowResponse> = {};
|
|
208
|
+
let failed = 0;
|
|
291
209
|
for (const result of detailResults) {
|
|
292
210
|
if (result.status === "fulfilled") {
|
|
293
211
|
const [id, data] = result.value;
|
|
294
212
|
models[id] = data;
|
|
213
|
+
} else {
|
|
214
|
+
failed++;
|
|
295
215
|
}
|
|
296
216
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
onProgress({
|
|
303
|
-
stage: "done",
|
|
304
|
-
current: Object.keys(models).length,
|
|
305
|
-
total: Object.keys(models).length,
|
|
306
|
-
message: "Done",
|
|
307
|
-
});
|
|
308
|
-
return models;
|
|
217
|
+
if (Object.keys(models).length === 0) {
|
|
218
|
+
throw new Error(`Failed to fetch model details${failed ? ` (${failed} failed)` : ""}`);
|
|
219
|
+
}
|
|
220
|
+
return { models, failed };
|
|
309
221
|
}
|
|
310
222
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
223
|
+
// --- refreshModels callback ---
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The `refreshModels` callback pi invokes for the "ollama-cloud" provider.
|
|
227
|
+
* Pi calls it twice per refresh: a restore phase (`allowNetwork: false`) before
|
|
228
|
+
* auth resolution, then a network phase (`allowNetwork: true`) only when a
|
|
229
|
+
* credential resolves. The composer swaps the return value into the model list
|
|
230
|
+
* on every invocation, so this must never return `[]`.
|
|
231
|
+
*
|
|
232
|
+
* The model fetch itself is keyless (public `/v1/models` and `/api/show`
|
|
233
|
+
* endpoints; `Authorization` is only added when `OLLAMA_API_KEY` is set). But
|
|
234
|
+
* pi only invokes this network phase when a credential resolves, so a
|
|
235
|
+
* credentialless user stays on `GENERATED_MODELS` until they configure a key.
|
|
236
|
+
* That is a non-issue in practice because a credentialless user cannot run
|
|
237
|
+
* models anyway.
|
|
238
|
+
*/
|
|
239
|
+
export async function refreshOllamaCatalog(context: RefreshModelsContext): Promise<ProviderModelConfig[]> {
|
|
240
|
+
// The fallback list: the persisted snapshot (copied) when non-empty, else the
|
|
241
|
+
// baked-in list. Guards against a stored empty catalog (e.g. a prior bad
|
|
242
|
+
// refresh) propagating [] across sessions. A mutable copy is returned because
|
|
243
|
+
// the stored list is `readonly` and the return type is a mutable array.
|
|
244
|
+
const fallback = context.stored?.models.length ? [...context.stored.models] : GENERATED_MODELS;
|
|
245
|
+
|
|
246
|
+
// Restore phase. Rehydrate from the persisted snapshot so removals stick
|
|
247
|
+
// across sessions; fall back to the baked-in list on first launch. Also the
|
|
248
|
+
// early-out for an already-aborted signal.
|
|
249
|
+
if (!context.allowNetwork || context.signal.aborted) {
|
|
250
|
+
return fallback;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Cooldown: skip the network fetch when the stored catalog was checked within
|
|
254
|
+
// the freshness window and the refresh isn't forced (mirrors pi-mono's
|
|
255
|
+
// remote-catalog-provider). A forced refresh (pi update --models) always fetches.
|
|
256
|
+
if (
|
|
257
|
+
!context.force &&
|
|
258
|
+
context.stored?.checkedAt !== undefined &&
|
|
259
|
+
Date.now() - context.stored.checkedAt < REFRESH_COOLDOWN_MS
|
|
260
|
+
) {
|
|
261
|
+
return fallback;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Network phase. The /v1/models and /api/show endpoints are publicly
|
|
265
|
+
// accessible and do not require authentication, so context.credential is
|
|
266
|
+
// intentionally not threaded into fetchModelIds/fetchModelDetails. Only
|
|
267
|
+
// the web tools (search, fetch) require an API key.
|
|
268
|
+
//
|
|
269
|
+
// Pi's model-selector aborts a catalog refresh after 15s
|
|
270
|
+
// (packages/coding-agent/src/modes/interactive/components/model-selector.ts
|
|
271
|
+
// in pi-mono), so a cold refresh must stay under that budget or the in-memory
|
|
272
|
+
// list won't update on the first picker-open. With ~18 models and 8 workers
|
|
273
|
+
// this is ~1s today; revisit if the catalog grows or the API slows.
|
|
274
|
+
let modelIds: string[];
|
|
275
|
+
let raw: Record<string, OllamaShowResponse>;
|
|
276
|
+
let failed = 0;
|
|
315
277
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
278
|
+
modelIds = await fetchModelIds(context.signal);
|
|
279
|
+
if (context.signal.aborted) {
|
|
280
|
+
return fallback;
|
|
281
|
+
}
|
|
282
|
+
const result = await refreshOllamaCloudModels(modelIds, context.signal);
|
|
283
|
+
raw = result.models;
|
|
284
|
+
failed = result.failed;
|
|
320
285
|
} catch (error) {
|
|
321
|
-
|
|
322
|
-
|
|
286
|
+
// Abort mid-flight returns the current baseline; any other error propagates
|
|
287
|
+
// and pi keeps the last good catalog (no publish was reached).
|
|
288
|
+
if (context.signal.aborted) {
|
|
289
|
+
return fallback;
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
if (context.signal.aborted) {
|
|
294
|
+
return fallback;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const models = assembleModels(raw);
|
|
298
|
+
// Guard: an empty assembled list (e.g. the live API returned no tools-capable
|
|
299
|
+
// models) must not be persisted or swapped in, or it would kill the provider
|
|
300
|
+
// for the cooldown window. Keep the last good catalog instead.
|
|
301
|
+
if (models.length === 0) {
|
|
302
|
+
return fallback;
|
|
323
303
|
}
|
|
304
|
+
|
|
305
|
+
// The store is typed to pi-ai's internal Model shape, so rehydrate the live
|
|
306
|
+
// list with the provider identity fields. A Model is structurally assignable
|
|
307
|
+
// to ProviderModelConfig, so the same list is returned to pi (the composer
|
|
308
|
+
// overrides provider/api/baseUrl on the in-memory swap regardless).
|
|
309
|
+
const persisted = models.map((model) => ({
|
|
310
|
+
...model,
|
|
311
|
+
provider: "ollama-cloud",
|
|
312
|
+
api: "openai-completions",
|
|
313
|
+
baseUrl: `${OLLAMA_BASE}/v1`,
|
|
314
|
+
}));
|
|
315
|
+
|
|
316
|
+
// Best-effort persistence into pi's FileModelsStore. The in-memory list swap
|
|
317
|
+
// happens automatically from the return value, so a failed store write must
|
|
318
|
+
// not prevent returning the fresh catalog.
|
|
319
|
+
if (failed === 0) {
|
|
320
|
+
// Fully successful: persist the fresh catalog.
|
|
321
|
+
try {
|
|
322
|
+
const published = await context.publish({ persist: { models: persisted, checkedAt: Date.now() } });
|
|
323
|
+
if (!published) {
|
|
324
|
+
console.warn("[pi-ollama-cloud] Catalog persist rejected (generation check failed or refresh superseded).");
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
// Persistence failure is non-fatal.
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
// Partial failure: keep the last-good catalog (if any) but advance checkedAt
|
|
331
|
+
// so the cooldown applies and a flaky catalog isn't re-fetched on every
|
|
332
|
+
// /model open, then surface the incomplete refresh. Mirrors pi-mono's
|
|
333
|
+
// remote-catalog-provider, which persists then throws on a transient failure;
|
|
334
|
+
// pi keeps the last-good catalog and reports the error.
|
|
335
|
+
if (context.stored?.models.length) {
|
|
336
|
+
try {
|
|
337
|
+
const published = await context.publish({ persist: { ...context.stored, checkedAt: Date.now() } });
|
|
338
|
+
if (!published) {
|
|
339
|
+
console.warn(
|
|
340
|
+
"[pi-ollama-cloud] Partial-failure persist rejected (generation check failed or refresh superseded).",
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
} catch {
|
|
344
|
+
// Persistence failure is non-fatal.
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
throw new Error(`Ollama Cloud catalog refresh incomplete: ${failed} model(s) failed`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return persisted;
|
|
324
351
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ollama-cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -20,11 +20,12 @@
|
|
|
20
20
|
],
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "https://github.com/fgrehm/pi-ollama-cloud"
|
|
23
|
+
"url": "git+https://github.com/fgrehm/pi-ollama-cloud.git"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
|
-
"check": "biome check --write .",
|
|
26
|
+
"check": "biome check --write . && tsgo --noEmit",
|
|
27
27
|
"lint": "biome check .",
|
|
28
|
+
"typecheck": "tsgo --noEmit",
|
|
28
29
|
"format": "biome format --write .",
|
|
29
30
|
"test": "vitest run",
|
|
30
31
|
"smoke:web-tools": "tsx scripts/smoke-web-tools.ts",
|
|
@@ -36,12 +37,15 @@
|
|
|
36
37
|
]
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
40
|
+
"@earendil-works/pi-ai": "*",
|
|
39
41
|
"@earendil-works/pi-coding-agent": "*",
|
|
40
42
|
"@earendil-works/pi-tui": "*",
|
|
41
43
|
"@sinclair/typebox": "*"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"@biomejs/biome": "2",
|
|
47
|
+
"@types/node": "^26.1.2",
|
|
48
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
45
49
|
"tsx": "^4.19.0",
|
|
46
50
|
"vitest": "^4.1.6"
|
|
47
51
|
}
|
package/pricing.generated.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-pricing.ts
|
|
2
2
|
// Do not edit manually.
|
|
3
|
-
// Generated: 2026-
|
|
3
|
+
// Generated: 2026-08-08T04:19:45.594Z
|
|
4
4
|
// Model count: 18
|
|
5
5
|
|
|
6
6
|
export interface ModelPrice {
|
|
@@ -11,21 +11,21 @@ export interface ModelPrice {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export const MODEL_PRICING: Record<string, ModelPrice> = {
|
|
14
|
-
"deepseek-v4-flash": { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
|
|
14
|
+
"deepseek-v4-flash:0731": { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
|
|
15
|
+
"deepseek-v4-flash:preview": { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
|
|
15
16
|
"deepseek-v4-pro": { input: 0.435, output: 0.87, cacheRead: 0.003625, cacheWrite: 0 },
|
|
16
|
-
"gemma4:31b": { input: 0.
|
|
17
|
+
"gemma4:31b": { input: 0.1, output: 0.34, cacheRead: 0.1, cacheWrite: 0 },
|
|
17
18
|
"glm-5.1": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
|
18
19
|
"glm-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
|
19
20
|
"gpt-oss:120b": { input: 0.037, output: 0.17, cacheRead: 0, cacheWrite: 0 },
|
|
20
21
|
"gpt-oss:20b": { input: 0.03, output: 0.13, cacheRead: 0.03, cacheWrite: 0 },
|
|
21
|
-
"kimi-k2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0 },
|
|
22
22
|
"kimi-k2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
|
|
23
23
|
"kimi-k2.7-code": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
|
|
24
|
-
"
|
|
24
|
+
"kimi-k3": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
|
|
25
25
|
"minimax-m2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 },
|
|
26
26
|
"minimax-m3": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
|
|
27
27
|
"mistral-large-3:675b": { input: 0.5, output: 1.5, cacheRead: 0, cacheWrite: 0 },
|
|
28
|
-
"nemotron-3-nano:30b": { input: 0.05, output: 0.2, cacheRead: 0, cacheWrite: 0 },
|
|
28
|
+
"nemotron-3-nano:30b": { input: 0.05, output: 0.2, cacheRead: 0.03, cacheWrite: 0 },
|
|
29
29
|
"nemotron-3-super": { input: 0.2, output: 0.8, cacheRead: 0, cacheWrite: 0 },
|
|
30
30
|
"nemotron-3-ultra": { input: 0.5, output: 2.5, cacheRead: 0.15, cacheWrite: 0 },
|
|
31
31
|
"qwen3.5:397b": { input: 0.6, output: 3.6, cacheRead: 0, cacheWrite: 0 },
|
package/web-tools.ts
CHANGED
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import {
|
|
20
|
+
type AgentToolResult,
|
|
20
21
|
type ExtensionAPI,
|
|
21
22
|
type ExtensionContext,
|
|
22
23
|
keyHint,
|
|
23
24
|
type Theme,
|
|
25
|
+
type ToolRenderResultOptions,
|
|
24
26
|
truncateToVisualLines,
|
|
25
27
|
} from "@earendil-works/pi-coding-agent";
|
|
26
28
|
import { type Component, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
@@ -41,7 +43,7 @@ interface SearchResponse {
|
|
|
41
43
|
interface FetchResponse {
|
|
42
44
|
title: string;
|
|
43
45
|
content: string;
|
|
44
|
-
links: string[];
|
|
46
|
+
links: string[] | null;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
// --- Helpers ---
|
|
@@ -63,68 +65,45 @@ export async function getCloudApiKey(ctx: Pick<ExtensionContext, "modelRegistry"
|
|
|
63
65
|
return (await ctx.modelRegistry.getApiKeyForProvider("ollama-cloud")) ?? process.env.OLLAMA_API_KEY;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
type: "text" as const,
|
|
71
|
-
text: "Error: No Ollama Cloud API key configured. Set OLLAMA_API_KEY or add to auth.json.",
|
|
72
|
-
},
|
|
73
|
-
],
|
|
74
|
-
isError: true,
|
|
75
|
-
};
|
|
68
|
+
/** Throw a no-API-key error. */
|
|
69
|
+
function noApiKeyError(): never {
|
|
70
|
+
throw new Error("No Ollama Cloud API key configured. Set OLLAMA_API_KEY or add to auth.json.");
|
|
76
71
|
}
|
|
77
72
|
|
|
78
|
-
/**
|
|
79
|
-
function searchError(status: number, error?: string) {
|
|
73
|
+
/** Throw a search error for a non-ok result, mapping distinct status codes. */
|
|
74
|
+
function searchError(status: number, error?: string): never {
|
|
80
75
|
if (status === 401 || status === 403) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
type: "text" as const,
|
|
85
|
-
text:
|
|
86
|
-
"Ollama Cloud search failed: authentication error. " + "Check your API key in OLLAMA_API_KEY or auth.json.",
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
isError: true,
|
|
90
|
-
};
|
|
76
|
+
throw new Error(
|
|
77
|
+
"Ollama Cloud search failed: authentication error. " + "Check your API key in OLLAMA_API_KEY or auth.json.",
|
|
78
|
+
);
|
|
91
79
|
}
|
|
92
80
|
if (status === 429) {
|
|
93
|
-
|
|
94
|
-
content: [{ type: "text" as const, text: "Ollama Cloud search failed: rate limited. Try again shortly." }],
|
|
95
|
-
isError: true,
|
|
96
|
-
};
|
|
81
|
+
throw new Error("Ollama Cloud search failed: rate limited. Try again shortly.");
|
|
97
82
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
83
|
+
if (status >= 500) {
|
|
84
|
+
throw new Error(`Ollama Cloud search failed: server error (status ${status}). Try again shortly.`);
|
|
85
|
+
}
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Ollama Cloud search failed: unexpected response (status ${status}${error ? `: ${error}` : ""}). Try again shortly.`,
|
|
88
|
+
);
|
|
102
89
|
}
|
|
103
90
|
|
|
104
|
-
/**
|
|
105
|
-
function fetchError(status: number, error?: string) {
|
|
91
|
+
/** Throw a fetch error for a non-ok result, mapping distinct status codes. */
|
|
92
|
+
function fetchError(status: number, error?: string): never {
|
|
106
93
|
if (status === 401 || status === 403) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
type: "text" as const,
|
|
111
|
-
text:
|
|
112
|
-
"Ollama Cloud fetch failed: authentication error. " + "Check your API key in OLLAMA_API_KEY or auth.json.",
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
|
-
isError: true,
|
|
116
|
-
};
|
|
94
|
+
throw new Error(
|
|
95
|
+
"Ollama Cloud fetch failed: authentication error. " + "Check your API key in OLLAMA_API_KEY or auth.json.",
|
|
96
|
+
);
|
|
117
97
|
}
|
|
118
98
|
if (status === 429) {
|
|
119
|
-
|
|
120
|
-
content: [{ type: "text" as const, text: "Ollama Cloud fetch failed: rate limited. Try again shortly." }],
|
|
121
|
-
isError: true,
|
|
122
|
-
};
|
|
99
|
+
throw new Error("Ollama Cloud fetch failed: rate limited. Try again shortly.");
|
|
123
100
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
101
|
+
if (status >= 500) {
|
|
102
|
+
throw new Error(`Ollama Cloud fetch failed: server error (status ${status}). Try again shortly.`);
|
|
103
|
+
}
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Ollama Cloud fetch failed: unexpected response (status ${status}${error ? `: ${error}` : ""}). Try again shortly.`,
|
|
106
|
+
);
|
|
128
107
|
}
|
|
129
108
|
|
|
130
109
|
const PREVIEW_LINES = 8;
|
|
@@ -135,17 +114,19 @@ const PREVIEW_LINES = 8;
|
|
|
135
114
|
*/
|
|
136
115
|
function createRenderResult() {
|
|
137
116
|
return (
|
|
138
|
-
result:
|
|
139
|
-
options:
|
|
117
|
+
result: AgentToolResult<unknown>,
|
|
118
|
+
options: ToolRenderResultOptions,
|
|
140
119
|
theme: Theme,
|
|
141
120
|
context: {
|
|
142
121
|
invalidate: () => void;
|
|
143
122
|
lastComponent: Component | undefined;
|
|
123
|
+
isError: boolean;
|
|
144
124
|
state: { cachedWidth?: number; cachedLines?: string[]; cachedSkipped?: number };
|
|
145
125
|
},
|
|
146
126
|
) => {
|
|
147
127
|
const state = context.state;
|
|
148
128
|
const output = result.content
|
|
129
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
149
130
|
.map((c) => c.text)
|
|
150
131
|
.join("")
|
|
151
132
|
.trim();
|
|
@@ -154,9 +135,9 @@ function createRenderResult() {
|
|
|
154
135
|
.map((line: string) => theme.fg("toolOutput", line))
|
|
155
136
|
.join("\n");
|
|
156
137
|
|
|
157
|
-
if (options.expanded ||
|
|
138
|
+
if (options.expanded || context.isError) {
|
|
158
139
|
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
159
|
-
text.setText(
|
|
140
|
+
text.setText(context.isError ? styledOutput : `\n${styledOutput}`);
|
|
160
141
|
return text;
|
|
161
142
|
}
|
|
162
143
|
|
|
@@ -185,16 +166,25 @@ function createRenderResult() {
|
|
|
185
166
|
};
|
|
186
167
|
}
|
|
187
168
|
|
|
188
|
-
/** Validate a parsed web_search response: must have a results array. */
|
|
189
|
-
function isSearchResponse(data: unknown): data is SearchResponse {
|
|
190
|
-
|
|
169
|
+
/** Validate a parsed web_search response: must have a results array of well-formed entries. */
|
|
170
|
+
export function isSearchResponse(data: unknown): data is SearchResponse {
|
|
171
|
+
if (data == null || typeof data !== "object") return false;
|
|
172
|
+
const results = (data as SearchResponse).results;
|
|
173
|
+
return (
|
|
174
|
+
Array.isArray(results) &&
|
|
175
|
+
results.every((r) => typeof r.title === "string" && typeof r.url === "string" && typeof r.content === "string")
|
|
176
|
+
);
|
|
191
177
|
}
|
|
192
178
|
|
|
193
|
-
/** Validate a parsed web_fetch response: must have string title/content and a links array. */
|
|
194
|
-
function isFetchResponse(data: unknown): data is FetchResponse {
|
|
179
|
+
/** Validate a parsed web_fetch response: must have string title/content and a links array of strings (or null). */
|
|
180
|
+
export function isFetchResponse(data: unknown): data is FetchResponse {
|
|
195
181
|
if (data == null || typeof data !== "object") return false;
|
|
196
182
|
const d = data as FetchResponse;
|
|
197
|
-
return
|
|
183
|
+
return (
|
|
184
|
+
typeof d.title === "string" &&
|
|
185
|
+
typeof d.content === "string" &&
|
|
186
|
+
(d.links === null || (Array.isArray(d.links) && d.links.every((l) => typeof l === "string")))
|
|
187
|
+
);
|
|
198
188
|
}
|
|
199
189
|
|
|
200
190
|
// --- Registrations ---
|
|
@@ -220,7 +210,9 @@ export function registerWebSearchTool(pi: ExtensionAPI) {
|
|
|
220
210
|
}),
|
|
221
211
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
222
212
|
const apiKey = await getCloudApiKey(ctx);
|
|
223
|
-
if (!apiKey)
|
|
213
|
+
if (!apiKey) {
|
|
214
|
+
noApiKeyError();
|
|
215
|
+
}
|
|
224
216
|
|
|
225
217
|
const res = await fetchJsonWithTimeout<SearchResponse>(
|
|
226
218
|
`${OLLAMA_BASE}/api/web_search`,
|
|
@@ -239,12 +231,11 @@ export function registerWebSearchTool(pi: ExtensionAPI) {
|
|
|
239
231
|
signal,
|
|
240
232
|
);
|
|
241
233
|
|
|
242
|
-
if (!res.ok)
|
|
234
|
+
if (!res.ok) {
|
|
235
|
+
searchError(res.status, res.error);
|
|
236
|
+
}
|
|
243
237
|
if (!isSearchResponse(res.data)) {
|
|
244
|
-
|
|
245
|
-
content: [{ type: "text", text: "Web search failed: unexpected response shape from the API." }],
|
|
246
|
-
isError: true,
|
|
247
|
-
};
|
|
238
|
+
throw new Error("Web search failed: unexpected response shape from the API.");
|
|
248
239
|
}
|
|
249
240
|
|
|
250
241
|
const formatted = res.data.results
|
|
@@ -277,7 +268,9 @@ export function registerWebFetchTool(pi: ExtensionAPI) {
|
|
|
277
268
|
}),
|
|
278
269
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
279
270
|
const apiKey = await getCloudApiKey(ctx);
|
|
280
|
-
if (!apiKey)
|
|
271
|
+
if (!apiKey) {
|
|
272
|
+
noApiKeyError();
|
|
273
|
+
}
|
|
281
274
|
|
|
282
275
|
const res = await fetchJsonWithTimeout<FetchResponse>(
|
|
283
276
|
`${OLLAMA_BASE}/api/web_fetch`,
|
|
@@ -293,12 +286,11 @@ export function registerWebFetchTool(pi: ExtensionAPI) {
|
|
|
293
286
|
signal,
|
|
294
287
|
);
|
|
295
288
|
|
|
296
|
-
if (!res.ok)
|
|
289
|
+
if (!res.ok) {
|
|
290
|
+
fetchError(res.status, res.error);
|
|
291
|
+
}
|
|
297
292
|
if (!isFetchResponse(res.data)) {
|
|
298
|
-
|
|
299
|
-
content: [{ type: "text", text: "Web fetch failed: unexpected response shape from the API." }],
|
|
300
|
-
isError: true,
|
|
301
|
-
};
|
|
293
|
+
throw new Error("Web fetch failed: unexpected response shape from the API.");
|
|
302
294
|
}
|
|
303
295
|
|
|
304
296
|
const data = res.data;
|