pi-ollama-cloud 0.7.0 → 0.9.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 +19 -1
- package/README.md +113 -37
- package/config.ts +4 -0
- package/index.ts +163 -120
- package/models.generated.ts +56 -56
- package/models.ts +162 -135
- package/package.json +8 -3
- package/pricing.generated.ts +6 -6
- package/usage.ts +170 -0
- package/utils.ts +36 -0
- package/web-tools.ts +42 -102
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [0.9.0] - 2026-08-11
|
|
6
|
+
|
|
7
|
+
- Add `/ollama-cloud-usage` command to show Ollama Cloud session (5h) and weekly (7d) usage limits, per-model request counts, and the 4-week activity cost, fetched from the undocumented `/api/usage` endpoint with the already-resolved API key.
|
|
8
|
+
- Add a footer usage status bar (`5h ▕███░░░░░░░▏ 34% 7d ▕████░░░░░░▏ 45%`) while an `ollama-cloud` model is active, refreshing every 5 minutes and after each agent turn (throttled so it never exceeds one /api/usage call per 5 minutes). Segments are colored by usage level (green <60%, yellow 60-79%, red 80%+). The quota-bar concept is inspired by `@entelligentsia/pi-ollama-cloud-usage-tracker`. Off by default; enable with `/ollama-usage-status on` or `"usageStatus": true` in `ollama-cloud.json`.
|
|
9
|
+
- Add `/ollama-usage-status [on|off|enable|disable]` to toggle the footer usage status bar at runtime (toggles without an argument).
|
|
10
|
+
- Document the exported usage API (`fetchUsage`, `formatUsageStatusColored`, `getCloudApiKey`, validators) so custom status bars can reuse it.
|
|
11
|
+
|
|
12
|
+
## [0.8.0] - 2026-08-09
|
|
13
|
+
|
|
14
|
+
- **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`.
|
|
15
|
+
- 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).
|
|
16
|
+
- 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).
|
|
17
|
+
- Requires a pi version with the `stored`/`publish` `RefreshModelsContext` (pi 0.84.0+). Add `@earendil-works/pi-ai` as a peer dependency.
|
|
18
|
+
- Web tools now throw on errors instead of returning an `isError` result, aligning with pi 0.84.0's `AgentToolResult` contract.
|
|
19
|
+
- 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.
|
|
20
|
+
- 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.
|
|
21
|
+
- Add `tsgo --noEmit` type-checking to `npm run check` and CI.
|
|
22
|
+
|
|
23
|
+
## [0.7.0] - 2026-07-18
|
|
6
24
|
|
|
7
25
|
- 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
26
|
- 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.
|
|
@@ -99,30 +97,20 @@ Extension settings can be set via JSON config files. Project-local settings over
|
|
|
99
97
|
| Setting | Type | Default | Description |
|
|
100
98
|
|---|---|---|---|
|
|
101
99
|
| `webTools` | boolean | `true` | Set to `false` to prevent `ollama_web_search` and `ollama_web_fetch` from being registered |
|
|
100
|
+
| `usageStatus` | boolean | `false` | Set to `true` to show the footer usage status bar (opt-in; enable at runtime with `/ollama-usage-status`) |
|
|
102
101
|
|
|
103
102
|
Example `ollama-cloud.json`:
|
|
104
103
|
|
|
105
104
|
```json
|
|
106
105
|
{
|
|
107
|
-
"webTools": false
|
|
106
|
+
"webTools": false,
|
|
107
|
+
"usageStatus": true
|
|
108
108
|
}
|
|
109
109
|
```
|
|
110
110
|
|
|
111
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
112
|
|
|
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
|
|
113
|
+
### 4. Select a model
|
|
126
114
|
|
|
127
115
|
Use `/model` or `Ctrl+L` to switch to an Ollama Cloud model. Models appear under the `ollama-cloud` provider.
|
|
128
116
|
|
|
@@ -135,9 +123,11 @@ The plugin uses two Ollama Cloud API endpoints to build the model list:
|
|
|
135
123
|
|
|
136
124
|
Only models with the `tools` capability are registered - these are the ones Pi can use for tool-calling.
|
|
137
125
|
|
|
138
|
-
The
|
|
126
|
+
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.
|
|
127
|
+
|
|
128
|
+
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.
|
|
139
129
|
|
|
140
|
-
Model metadata is derived from the
|
|
130
|
+
Model metadata is derived from the `/api/show` response:
|
|
141
131
|
|
|
142
132
|
| Field | Source |
|
|
143
133
|
|---|---|
|
|
@@ -146,7 +136,7 @@ Model metadata is derived from the cached data:
|
|
|
146
136
|
| `input` | `["text", "image"]` if `capabilities` includes `"vision"`, else `["text"]` |
|
|
147
137
|
| `contextWindow` | `model_info.*.context_length` (falls back to 128000) |
|
|
148
138
|
| `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
|
|
139
|
+
| `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
140
|
|
|
151
141
|
### Thinking level mapping
|
|
152
142
|
|
|
@@ -162,14 +152,6 @@ Pi's thinking levels are mapped to Ollama Cloud's OpenAI-compatible `reasoning_e
|
|
|
162
152
|
|
|
163
153
|
See [docs/think-experiment.md](docs/think-experiment.md) for the testing methodology and results.
|
|
164
154
|
|
|
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
155
|
## Tools
|
|
174
156
|
|
|
175
157
|
| Tool | Description |
|
|
@@ -183,19 +165,100 @@ Both tools use the same Ollama Cloud API key configured for the provider. No loc
|
|
|
183
165
|
|
|
184
166
|
| Command | Description |
|
|
185
167
|
|---|---|
|
|
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
168
|
| `/ollama-webtools [on\|off\|enable\|disable]` | Enable or disable the `ollama_web_search` and `ollama_web_fetch` tools. Toggles if no argument given. |
|
|
169
|
+
| `/ollama-cloud-usage` | Show Ollama Cloud session (5h) and weekly (7d) usage limits, per-model request counts, and the 4-week activity cost. |
|
|
170
|
+
| `/ollama-usage-status [on\|off\|enable\|disable]` | Enable or disable the footer usage status bar. Toggles if no argument given. |
|
|
171
|
+
|
|
172
|
+
## Usage status bar
|
|
173
|
+
|
|
174
|
+
While an `ollama-cloud` model is the active provider, the footer shows a compact
|
|
175
|
+
live usage readout (`5h ▕███░░░░░░░▏ 34% 7d ▕████░░░░░░▏ 45%`) that refreshes
|
|
176
|
+
every 5 minutes and after each agent turn (but no more often than every 5 minutes). Each segment is colored by how close
|
|
177
|
+
it is to the cap: green below 60%, yellow at 60-79%, red at 80%+. It reads the
|
|
178
|
+
same undocumented `/api/usage` endpoint as `/ollama-cloud-usage` and clears
|
|
179
|
+
itself on transient errors or when you switch to a non-Ollama-Cloud provider.
|
|
180
|
+
|
|
181
|
+
It is off by default. Enable it at runtime with `/ollama-usage-status on`, or
|
|
182
|
+
enable it by default with `"usageStatus": true` in `ollama-cloud.json`. If the
|
|
183
|
+
bar never appears after enabling, run `/ollama-cloud-usage` to see the
|
|
184
|
+
underlying error (e.g. a misconfigured API key).
|
|
185
|
+
|
|
186
|
+
The quota-bar concept is inspired by
|
|
187
|
+
[`@entelligentsia/pi-ollama-cloud-usage-tracker`](https://github.com/Entelligentsia/pi-ollama-cloud-usage-tracker),
|
|
188
|
+
but this extension fetches usage from the `/api/usage` endpoint with the API key
|
|
189
|
+
it already resolves, rather than scraping the settings page with Chrome cookies.
|
|
190
|
+
|
|
191
|
+
## Usage API for custom status bars
|
|
192
|
+
|
|
193
|
+
The usage data plane is exported so you can plug it into your own footer or
|
|
194
|
+
status bar instead of (or alongside) the built-in one. The relevant modules ship
|
|
195
|
+
with the package and are importable directly:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { fetchUsage, formatUsage, formatUsageStatusColored } from "pi-ollama-cloud/usage.ts";
|
|
199
|
+
import { getCloudApiKey } from "pi-ollama-cloud/utils.ts";
|
|
200
|
+
import type { UsageData } from "pi-ollama-cloud/usage.ts";
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
| Export | Description |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `fetchUsage(apiKey, signal?)` | Fetch the raw `/api/usage` data, returning a typed `UsageData`. Throws a status-mapped error on 401/403/429/404/5xx. |
|
|
206
|
+
| `formatUsageStatusColored(theme, data)` | One-line status string with quota bars, colored by usage level. Takes a `Theme` (e.g. `ctx.ui.theme`). |
|
|
207
|
+
| `formatUsage(data)` | Multi-line human-readable output (percentages, per-model request counts, activity cost). |
|
|
208
|
+
| `getCloudApiKey(ctx)` | Resolve the Ollama Cloud API key the same way the extension does. |
|
|
209
|
+
| `isUsageResponse(data)` / `isUsageLimit(data)` | Validators for parsing the raw response yourself. |
|
|
210
|
+
|
|
211
|
+
Example custom status bar:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const apiKey = await getCloudApiKey(ctx);
|
|
215
|
+
const data = await fetchUsage(apiKey);
|
|
216
|
+
ctx.ui.setStatus("my-usage", formatUsageStatusColored(ctx.ui.theme, data));
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Note that the package ships raw TypeScript sources (no build step), so submodule
|
|
220
|
+
imports use the `.ts` extension, matching how the extension imports internally.
|
|
188
221
|
|
|
189
222
|
## Development
|
|
190
223
|
|
|
191
224
|
```bash
|
|
192
|
-
npm install # install devDependencies
|
|
193
|
-
npm run check # lint + format
|
|
194
|
-
npm run lint
|
|
195
|
-
npm run
|
|
225
|
+
npm install # install devDependencies
|
|
226
|
+
npm run check # lint + format + type-check (auto-fix)
|
|
227
|
+
npm run lint # lint only (no fixes)
|
|
228
|
+
npm run typecheck # type-check only (tsgo --noEmit)
|
|
229
|
+
npm run format # format only
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
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.
|
|
233
|
+
|
|
234
|
+
### Testing local changes
|
|
235
|
+
|
|
236
|
+
Static checks (no API key needed):
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
npm install
|
|
240
|
+
npm run check # lint + format + type-check
|
|
241
|
+
npm run test # unit tests
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Live smoke against the real API (needs an `OLLAMA_API_KEY` or an `ollama-cloud` entry in `auth.json`):
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# Run pi with the local extension, no install required. The --no-* flags isolate
|
|
248
|
+
# the run from other installed extensions, skills, prompt templates, themes,
|
|
249
|
+
# context files, and session storage so only the local checkout is exercised
|
|
250
|
+
pi --no-extensions --no-skills --no-prompt-templates --no-themes --no-context-files --no-session \
|
|
251
|
+
-e ./index.ts --model "ollama-cloud/gemma4:31b" --no-tools -p "Say hi in one word"
|
|
252
|
+
|
|
253
|
+
# Verify thinking is suppressed when off
|
|
254
|
+
pi --no-extensions --no-skills --no-prompt-templates --no-themes --no-context-files --no-session \
|
|
255
|
+
-e ./index.ts --thinking off --model "ollama-cloud/glm-5.2" --no-tools --mode json -p 'hi'
|
|
256
|
+
|
|
257
|
+
# Web tools
|
|
258
|
+
npm run smoke:web-tools
|
|
196
259
|
```
|
|
197
260
|
|
|
198
|
-
The
|
|
261
|
+
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
262
|
|
|
200
263
|
## How is this different from `ollama launch pi`?
|
|
201
264
|
|
|
@@ -211,7 +274,7 @@ The project uses [Biome](https://biomejs.dev/) for linting and formatting (2-spa
|
|
|
211
274
|
| **Authentication** | Handled by the local server (sign-in flow via `ollama`) | Ollama Cloud API key (set via `OLLAMA_API_KEY` or `auth.json`) |
|
|
212
275
|
| **Model discovery** | Interactive picker with curated recommendations + pulled models | Dynamic - fetches all available cloud models with tool support from the API |
|
|
213
276
|
| **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
|
|
277
|
+
| **Setup effort** | One command: `ollama launch pi` | Install extension + API key |
|
|
215
278
|
| **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
279
|
|
|
217
280
|
**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 +292,11 @@ npm version minor # or patch, or major
|
|
|
229
292
|
git push --tags
|
|
230
293
|
```
|
|
231
294
|
|
|
295
|
+
Because the model catalog refreshes automatically at runtime, a release is **not** needed to ship new models. Publish only when:
|
|
296
|
+
|
|
297
|
+
- 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`).
|
|
298
|
+
- Pricing changes: models.dev prices updated, or a new model needs an `OLLAMA_TO_MODELSDEV` mapping line (regenerate `pricing.generated.ts`).
|
|
299
|
+
|
|
232
300
|
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
301
|
|
|
234
302
|
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 +308,15 @@ The workflow uses npm's [trusted publishing](https://docs.npmjs.com/trusted-publ
|
|
|
240
308
|
|
|
241
309
|
Each publish also gets automatic [provenance attestation](https://docs.npmjs.com/generating-provenance-statements).
|
|
242
310
|
|
|
311
|
+
## Upgrading
|
|
312
|
+
|
|
313
|
+
Since 0.8.0:
|
|
314
|
+
|
|
315
|
+
- The `/ollama-cloud-refresh` command is removed. Models refresh automatically on startup, `/model` open, and `pi update --models`.
|
|
316
|
+
- 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`.
|
|
317
|
+
- Requires a pi version with the native `refreshModels` API (pi 0.84.0+).
|
|
318
|
+
|
|
243
319
|
## Notes
|
|
244
320
|
|
|
245
|
-
- The fetch timeout is 10 seconds per request. On slow connections, some model detail fetches may time out
|
|
321
|
+
- 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
322
|
- `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
CHANGED
|
@@ -25,12 +25,15 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
25
25
|
export interface OllamaCloudConfig {
|
|
26
26
|
/** When false, ollama_web_search and ollama_web_fetch tools are not registered. Default: true. */
|
|
27
27
|
webTools?: boolean;
|
|
28
|
+
/** When true, the footer usage status bar is shown. Default: false (opt-in; enable with /ollama-usage-status). */
|
|
29
|
+
usageStatus?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
// --- Defaults ---
|
|
31
33
|
|
|
32
34
|
const DEFAULT_CONFIG: OllamaCloudConfig = {
|
|
33
35
|
webTools: true,
|
|
36
|
+
usageStatus: false,
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
// --- Validation ---
|
|
@@ -38,6 +41,7 @@ const DEFAULT_CONFIG: OllamaCloudConfig = {
|
|
|
38
41
|
/** Allowed config keys and their expected types for runtime validation. */
|
|
39
42
|
const CONFIG_SCHEMA: Record<keyof OllamaCloudConfig, "boolean"> = {
|
|
40
43
|
webTools: "boolean",
|
|
44
|
+
usageStatus: "boolean",
|
|
41
45
|
};
|
|
42
46
|
|
|
43
47
|
/**
|
package/index.ts
CHANGED
|
@@ -1,144 +1,63 @@
|
|
|
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, ExtensionContext } 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
|
-
|
|
35
|
-
|
|
36
|
-
OLLAMA_BASE,
|
|
37
|
-
type RefreshProgress,
|
|
38
|
-
readCacheState,
|
|
39
|
-
writeCache,
|
|
40
|
-
} from "./models.ts";
|
|
30
|
+
import { OLLAMA_BASE, refreshOllamaCatalog } from "./models.ts";
|
|
31
|
+
import { fetchUsage, formatUsage, formatUsageStatusColored } from "./usage.ts";
|
|
32
|
+
import { getCloudApiKey } from "./utils.ts";
|
|
41
33
|
import { registerWebFetchTool, registerWebSearchTool } from "./web-tools.ts";
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
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";
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the new enabled state for /ollama-usage-status from its argument.
|
|
37
|
+
* Exported for unit testing.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveUsageStatusToggle(arg: string, current: boolean): { enabled: boolean; error?: string } {
|
|
40
|
+
const a = arg.trim().toLowerCase();
|
|
41
|
+
if (a === "on" || a === "enable") return { enabled: true };
|
|
42
|
+
if (a === "off" || a === "disable") return { enabled: false };
|
|
43
|
+
if (a === "") return { enabled: !current };
|
|
64
44
|
return {
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
},
|
|
45
|
+
enabled: current,
|
|
46
|
+
error: `Unknown argument "${arg.trim()}". Usage: /ollama-usage-status [on|off|enable|disable]`,
|
|
87
47
|
};
|
|
88
48
|
}
|
|
89
49
|
|
|
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
50
|
// --- Main ---
|
|
120
51
|
|
|
121
52
|
export default async function (pi: ExtensionAPI) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
}
|
|
53
|
+
pi.registerProvider("ollama-cloud", {
|
|
54
|
+
name: "Ollama Cloud",
|
|
55
|
+
baseUrl: `${OLLAMA_BASE}/v1`,
|
|
56
|
+
apiKey: "$OLLAMA_API_KEY",
|
|
57
|
+
api: "openai-completions",
|
|
58
|
+
models: GENERATED_MODELS,
|
|
59
|
+
refreshModels: refreshOllamaCatalog,
|
|
60
|
+
});
|
|
142
61
|
|
|
143
62
|
// --- Web Tools Management ---
|
|
144
63
|
|
|
@@ -180,21 +99,25 @@ export default async function (pi: ExtensionAPI) {
|
|
|
180
99
|
}
|
|
181
100
|
}
|
|
182
101
|
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
102
|
+
// Config is read once per extension factory invocation (on the first
|
|
103
|
+
// session_start). The factory is re-invoked on /new, /fork, /resume, and
|
|
104
|
+
// /reload, so runtime toggles (e.g. /ollama-webtools, /ollama-usage-status)
|
|
105
|
+
// reset to the config default on each session restart. Restart pi or /reload
|
|
106
|
+
// to pick up config file changes.
|
|
107
|
+
let configLoaded = false;
|
|
188
108
|
let webToolsEnabled = false;
|
|
109
|
+
let usageStatusEnabled = false;
|
|
189
110
|
|
|
190
111
|
pi.on("session_start", async (_event, ctx) => {
|
|
191
|
-
if (!
|
|
192
|
-
|
|
112
|
+
if (!configLoaded) {
|
|
113
|
+
configLoaded = true;
|
|
193
114
|
const config = loadConfig(ctx.cwd);
|
|
194
115
|
if (config.webTools !== false) {
|
|
195
116
|
webToolsEnabled = true;
|
|
196
117
|
ensureWebToolsRegistered();
|
|
197
118
|
}
|
|
119
|
+
// The status bar is opt-in: enabled only when the config explicitly sets it true.
|
|
120
|
+
usageStatusEnabled = config.usageStatus === true;
|
|
198
121
|
}
|
|
199
122
|
// On every session start (including resume/fork/new), re-apply the
|
|
200
123
|
// runtime state. Tools may have been unregistered during teardown.
|
|
@@ -202,6 +125,126 @@ export default async function (pi: ExtensionAPI) {
|
|
|
202
125
|
ensureWebToolsRegistered();
|
|
203
126
|
setWebToolsActive(true);
|
|
204
127
|
}
|
|
128
|
+
// Start the usage status bar when ollama-cloud is the active provider.
|
|
129
|
+
if (usageStatusEnabled && isOllamaCloud(ctx)) {
|
|
130
|
+
startUsageStatus(ctx);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// --- Usage Command ---
|
|
135
|
+
|
|
136
|
+
pi.registerCommand("ollama-cloud-usage", {
|
|
137
|
+
description: "Show Ollama Cloud session and weekly usage limits.",
|
|
138
|
+
handler: async (_args, ctx) => {
|
|
139
|
+
const apiKey = await getCloudApiKey(ctx);
|
|
140
|
+
if (!apiKey) {
|
|
141
|
+
ctx.ui.notify("No Ollama Cloud API key configured. Set OLLAMA_API_KEY or add to auth.json.", "error");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const data = await fetchUsage(apiKey);
|
|
146
|
+
ctx.ui.notify(formatUsage(data), "info");
|
|
147
|
+
} catch (err) {
|
|
148
|
+
ctx.ui.notify(err instanceof Error ? err.message : String(err), "error");
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// --- Usage Status Bar ---
|
|
154
|
+
|
|
155
|
+
// Footer status showing live session/weekly usage while ollama-cloud is the
|
|
156
|
+
// active provider. Refreshes on a 5-minute timer; agent_end also triggers a
|
|
157
|
+
// refresh but is throttled to the same cooldown so a turn never hammers the
|
|
158
|
+
// undocumented /api/usage endpoint. The quota-bar concept is inspired by
|
|
159
|
+
// @entelligentsia/pi-ollama-cloud-usage-tracker.
|
|
160
|
+
const USAGE_STATUS_KEY = "ollama-usage";
|
|
161
|
+
const USAGE_REFRESH_MS = 5 * 60_000;
|
|
162
|
+
let usageTimer: ReturnType<typeof setInterval> | null = null;
|
|
163
|
+
let usageActive = false;
|
|
164
|
+
// Timestamp (ms) of the most recent refresh attempt; gates the agent_end
|
|
165
|
+
// refresh so it fires at most once per cooldown. Set when a fetch starts, so
|
|
166
|
+
// a failing endpoint is also throttled, not just a successful one.
|
|
167
|
+
let lastRefreshAt = 0;
|
|
168
|
+
|
|
169
|
+
async function refreshUsageStatus(ctx: ExtensionContext) {
|
|
170
|
+
try {
|
|
171
|
+
const apiKey = await getCloudApiKey(ctx);
|
|
172
|
+
if (!apiKey) {
|
|
173
|
+
ctx.ui.setStatus(USAGE_STATUS_KEY, undefined);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
lastRefreshAt = Date.now();
|
|
177
|
+
const data = await fetchUsage(apiKey);
|
|
178
|
+
ctx.ui.setStatus(USAGE_STATUS_KEY, formatUsageStatusColored(ctx.ui.theme, data));
|
|
179
|
+
} catch {
|
|
180
|
+
// Transient errors (undocumented endpoint, network) should not spam the
|
|
181
|
+
// footer; clear the status and retry on the next refresh.
|
|
182
|
+
ctx.ui.setStatus(USAGE_STATUS_KEY, undefined);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function startUsageStatus(ctx: ExtensionContext) {
|
|
187
|
+
if (usageActive) return;
|
|
188
|
+
// The status bar is TUI-only; skip the fetch and timer in print/json/rpc.
|
|
189
|
+
if (ctx.mode !== "tui") return;
|
|
190
|
+
usageActive = true;
|
|
191
|
+
refreshUsageStatus(ctx);
|
|
192
|
+
usageTimer = setInterval(() => refreshUsageStatus(ctx), USAGE_REFRESH_MS);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function stopUsageStatus(ctx: ExtensionContext) {
|
|
196
|
+
usageActive = false;
|
|
197
|
+
if (usageTimer) {
|
|
198
|
+
clearInterval(usageTimer);
|
|
199
|
+
usageTimer = null;
|
|
200
|
+
}
|
|
201
|
+
ctx.ui.setStatus(USAGE_STATUS_KEY, undefined);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isOllamaCloud(ctx: ExtensionContext): boolean {
|
|
205
|
+
return ctx.model?.provider === "ollama-cloud";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
209
|
+
if (usageStatusEnabled && isOllamaCloud(ctx)) {
|
|
210
|
+
startUsageStatus(ctx);
|
|
211
|
+
} else {
|
|
212
|
+
stopUsageStatus(ctx);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
217
|
+
// Throttle the after-turn refresh to the same cooldown as the timer so a
|
|
218
|
+
// burst of turns never exceeds one /api/usage call per 5 minutes.
|
|
219
|
+
if (usageActive && isOllamaCloud(ctx) && Date.now() - lastRefreshAt >= USAGE_REFRESH_MS) {
|
|
220
|
+
await refreshUsageStatus(ctx);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
225
|
+
stopUsageStatus(ctx);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
pi.registerCommand("ollama-usage-status", {
|
|
229
|
+
description:
|
|
230
|
+
"Enable or disable the Ollama Cloud usage status bar. " +
|
|
231
|
+
"Accepts optional argument: on/off/enable/disable. Without argument, toggles.",
|
|
232
|
+
handler: async (args, ctx) => {
|
|
233
|
+
const { enabled, error } = resolveUsageStatusToggle(args, usageStatusEnabled);
|
|
234
|
+
if (error) {
|
|
235
|
+
ctx.ui.notify(error, "error");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
usageStatusEnabled = enabled;
|
|
239
|
+
|
|
240
|
+
if (usageStatusEnabled && isOllamaCloud(ctx)) {
|
|
241
|
+
startUsageStatus(ctx);
|
|
242
|
+
} else {
|
|
243
|
+
stopUsageStatus(ctx);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
ctx.ui.notify(`Ollama Cloud usage status: ${usageStatusEnabled ? "enabled" : "disabled"}`, "info");
|
|
247
|
+
},
|
|
205
248
|
});
|
|
206
249
|
|
|
207
250
|
// Only register the runtime toggle command when the env var doesn't force tools off.
|