pi-ollama-cloud 0.8.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 +7 -0
- package/README.md +56 -2
- package/config.ts +4 -0
- package/index.ts +149 -8
- package/package.json +2 -1
- package/usage.ts +170 -0
- package/utils.ts +36 -0
- package/web-tools.ts +3 -55
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
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
|
+
|
|
5
12
|
## [0.8.0] - 2026-08-09
|
|
6
13
|
|
|
7
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`.
|
package/README.md
CHANGED
|
@@ -97,12 +97,14 @@ Extension settings can be set via JSON config files. Project-local settings over
|
|
|
97
97
|
| Setting | Type | Default | Description |
|
|
98
98
|
|---|---|---|---|
|
|
99
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`) |
|
|
100
101
|
|
|
101
102
|
Example `ollama-cloud.json`:
|
|
102
103
|
|
|
103
104
|
```json
|
|
104
105
|
{
|
|
105
|
-
"webTools": false
|
|
106
|
+
"webTools": false,
|
|
107
|
+
"usageStatus": true
|
|
106
108
|
}
|
|
107
109
|
```
|
|
108
110
|
|
|
@@ -164,6 +166,58 @@ Both tools use the same Ollama Cloud API key configured for the provider. No loc
|
|
|
164
166
|
| Command | Description |
|
|
165
167
|
|---|---|
|
|
166
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.
|
|
167
221
|
|
|
168
222
|
## Development
|
|
169
223
|
|
|
@@ -192,7 +246,7 @@ Live smoke against the real API (needs an `OLLAMA_API_KEY` or an `ollama-cloud`
|
|
|
192
246
|
```bash
|
|
193
247
|
# Run pi with the local extension, no install required. The --no-* flags isolate
|
|
194
248
|
# the run from other installed extensions, skills, prompt templates, themes,
|
|
195
|
-
# context files, and session storage so only the local checkout is exercised
|
|
249
|
+
# context files, and session storage so only the local checkout is exercised
|
|
196
250
|
pi --no-extensions --no-skills --no-prompt-templates --no-themes --no-context-files --no-session \
|
|
197
251
|
-e ./index.ts --model "ollama-cloud/gemma4:31b" --no-tools -p "Say hi in one word"
|
|
198
252
|
|
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
|
@@ -24,12 +24,29 @@
|
|
|
24
24
|
* Only models with "tools" capability are registered.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
28
28
|
import { loadConfig, resolveWebToolsEnv } from "./config.ts";
|
|
29
29
|
import { GENERATED_MODELS } from "./models.generated.ts";
|
|
30
30
|
import { OLLAMA_BASE, refreshOllamaCatalog } from "./models.ts";
|
|
31
|
+
import { fetchUsage, formatUsage, formatUsageStatusColored } from "./usage.ts";
|
|
32
|
+
import { getCloudApiKey } from "./utils.ts";
|
|
31
33
|
import { registerWebFetchTool, registerWebSearchTool } from "./web-tools.ts";
|
|
32
34
|
|
|
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 };
|
|
44
|
+
return {
|
|
45
|
+
enabled: current,
|
|
46
|
+
error: `Unknown argument "${arg.trim()}". Usage: /ollama-usage-status [on|off|enable|disable]`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
// --- Main ---
|
|
34
51
|
|
|
35
52
|
export default async function (pi: ExtensionAPI) {
|
|
@@ -82,21 +99,25 @@ export default async function (pi: ExtensionAPI) {
|
|
|
82
99
|
}
|
|
83
100
|
}
|
|
84
101
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
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;
|
|
90
108
|
let webToolsEnabled = false;
|
|
109
|
+
let usageStatusEnabled = false;
|
|
91
110
|
|
|
92
111
|
pi.on("session_start", async (_event, ctx) => {
|
|
93
|
-
if (!
|
|
94
|
-
|
|
112
|
+
if (!configLoaded) {
|
|
113
|
+
configLoaded = true;
|
|
95
114
|
const config = loadConfig(ctx.cwd);
|
|
96
115
|
if (config.webTools !== false) {
|
|
97
116
|
webToolsEnabled = true;
|
|
98
117
|
ensureWebToolsRegistered();
|
|
99
118
|
}
|
|
119
|
+
// The status bar is opt-in: enabled only when the config explicitly sets it true.
|
|
120
|
+
usageStatusEnabled = config.usageStatus === true;
|
|
100
121
|
}
|
|
101
122
|
// On every session start (including resume/fork/new), re-apply the
|
|
102
123
|
// runtime state. Tools may have been unregistered during teardown.
|
|
@@ -104,6 +125,126 @@ export default async function (pi: ExtensionAPI) {
|
|
|
104
125
|
ensureWebToolsRegistered();
|
|
105
126
|
setWebToolsActive(true);
|
|
106
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
|
+
},
|
|
107
248
|
});
|
|
108
249
|
|
|
109
250
|
// Only register the runtime toggle command when the env var doesn't force tools off.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ollama-cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"models.generated.ts",
|
|
13
13
|
"pricing.generated.ts",
|
|
14
14
|
"thinking-levels.ts",
|
|
15
|
+
"usage.ts",
|
|
15
16
|
"utils.ts",
|
|
16
17
|
"web-tools.ts",
|
|
17
18
|
"CHANGELOG.md",
|
package/usage.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama Cloud usage data plane: fetch and format /api/usage.
|
|
3
|
+
*
|
|
4
|
+
* Self-contained module. Depends on:
|
|
5
|
+
* - models.ts - only for OLLAMA_BASE URL constant
|
|
6
|
+
* - utils.ts - fetchJsonWithTimeout
|
|
7
|
+
* Does NOT depend on provider registration, model fetching, or API key
|
|
8
|
+
* resolution (the caller resolves the key and passes it in).
|
|
9
|
+
*
|
|
10
|
+
* The /api/usage endpoint is undocumented and could change or disappear. The
|
|
11
|
+
* fetch degrades gracefully: distinct HTTP statuses map to distinct
|
|
12
|
+
* user-facing errors, and a malformed body raises a clear error rather than
|
|
13
|
+
* crashing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { OLLAMA_BASE } from "./models.ts";
|
|
18
|
+
import { fetchJsonWithTimeout, httpError } from "./utils.ts";
|
|
19
|
+
|
|
20
|
+
// --- Types ---
|
|
21
|
+
|
|
22
|
+
export interface UsageModel {
|
|
23
|
+
name: string;
|
|
24
|
+
request_count: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UsageLimit {
|
|
28
|
+
/** Fraction of the plan's cap, 0-1 (not tokens). */
|
|
29
|
+
usage: number;
|
|
30
|
+
/** Per-model request counts (not token counts). */
|
|
31
|
+
models: UsageModel[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UsageActivity {
|
|
35
|
+
cost?: string;
|
|
36
|
+
period?: {
|
|
37
|
+
type?: string;
|
|
38
|
+
starting_at?: string;
|
|
39
|
+
ending_at?: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface UsageData {
|
|
44
|
+
limits: {
|
|
45
|
+
session: UsageLimit;
|
|
46
|
+
weekly: UsageLimit;
|
|
47
|
+
};
|
|
48
|
+
activity?: UsageActivity;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- Constants ---
|
|
52
|
+
|
|
53
|
+
const USAGE_TIMEOUT_MS = 10000;
|
|
54
|
+
|
|
55
|
+
// --- Validation ---
|
|
56
|
+
|
|
57
|
+
/** Validate a single usage limit: a 0-1 fraction plus per-model request counts. */
|
|
58
|
+
export function isUsageLimit(data: unknown): data is UsageLimit {
|
|
59
|
+
if (data == null || typeof data !== "object") return false;
|
|
60
|
+
const d = data as UsageLimit;
|
|
61
|
+
return (
|
|
62
|
+
typeof d.usage === "number" &&
|
|
63
|
+
Array.isArray(d.models) &&
|
|
64
|
+
d.models.every(
|
|
65
|
+
(m) =>
|
|
66
|
+
m != null &&
|
|
67
|
+
typeof m === "object" &&
|
|
68
|
+
typeof (m as UsageModel).name === "string" &&
|
|
69
|
+
typeof (m as UsageModel).request_count === "number",
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Validate a parsed /api/usage response: must have session and weekly limits. */
|
|
75
|
+
export function isUsageResponse(data: unknown): data is UsageData {
|
|
76
|
+
if (data == null || typeof data !== "object") return false;
|
|
77
|
+
const d = data as UsageData;
|
|
78
|
+
return (
|
|
79
|
+
d.limits != null && typeof d.limits === "object" && isUsageLimit(d.limits.session) && isUsageLimit(d.limits.weekly)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- Fetch ---
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fetch Ollama Cloud usage from the undocumented /api/usage endpoint.
|
|
87
|
+
* The caller resolves the API key and passes it in.
|
|
88
|
+
*/
|
|
89
|
+
export async function fetchUsage(apiKey: string, externalSignal?: AbortSignal): Promise<UsageData> {
|
|
90
|
+
const res = await fetchJsonWithTimeout<UsageData>(
|
|
91
|
+
`${OLLAMA_BASE}/api/usage`,
|
|
92
|
+
{
|
|
93
|
+
method: "GET",
|
|
94
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
95
|
+
},
|
|
96
|
+
USAGE_TIMEOUT_MS,
|
|
97
|
+
externalSignal,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
// The 404 case is specific to this undocumented endpoint: it may have
|
|
102
|
+
// changed or disappeared, so surface that distinctly before the shared
|
|
103
|
+
// status mapping.
|
|
104
|
+
if (res.status === 404) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Ollama Cloud usage failed: the /api/usage endpoint is unavailable (status 404). " +
|
|
107
|
+
"It is undocumented and may have changed.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
httpError("usage", res.status, res.error);
|
|
111
|
+
}
|
|
112
|
+
if (!isUsageResponse(res.data)) {
|
|
113
|
+
throw new Error("Ollama Cloud usage failed: unexpected response shape from the API.");
|
|
114
|
+
}
|
|
115
|
+
return res.data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Formatting ---
|
|
119
|
+
|
|
120
|
+
/** Clamp a 0-1 usage fraction to a 0-100 percentage for display. */
|
|
121
|
+
function usagePercent(usage: number): number {
|
|
122
|
+
if (!Number.isFinite(usage)) return 0;
|
|
123
|
+
return Math.min(Math.max(Math.round(usage * 100), 0), 100);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Format usage for the /ollama-cloud-usage command output. */
|
|
127
|
+
export function formatUsage(data: UsageData): string {
|
|
128
|
+
const lines: string[] = ["Ollama Cloud usage:"];
|
|
129
|
+
|
|
130
|
+
const sessionPct = usagePercent(data.limits.session.usage);
|
|
131
|
+
lines.push(` Session (5h): ${sessionPct}%`);
|
|
132
|
+
for (const m of data.limits.session.models) {
|
|
133
|
+
lines.push(` - ${m.name}: ${m.request_count} request${m.request_count === 1 ? "" : "s"}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const weeklyPct = usagePercent(data.limits.weekly.usage);
|
|
137
|
+
lines.push(` Weekly (7d): ${weeklyPct}%`);
|
|
138
|
+
for (const m of data.limits.weekly.models) {
|
|
139
|
+
lines.push(` - ${m.name}: ${m.request_count} request${m.request_count === 1 ? "" : "s"}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (typeof data.activity?.cost === "string") {
|
|
143
|
+
lines.push(` Activity (4wk): $${data.activity.cost}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Render a 10-character quota bar for a 0-100 percentage. */
|
|
150
|
+
function quotaBar(pct: number): string {
|
|
151
|
+
const filled = Math.min(Math.max(Math.floor(pct / 10), 0), 10);
|
|
152
|
+
return `▕${"█".repeat(filled)}${"░".repeat(10 - filled)}▏`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Color a single usage segment by how close it is to the cap. */
|
|
156
|
+
function colorSegment(theme: Theme, label: string, pct: number): string {
|
|
157
|
+
const color = pct >= 80 ? "error" : pct >= 60 ? "warning" : "success";
|
|
158
|
+
return theme.fg(color, `${label} ${quotaBar(pct)} ${pct}%`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Compact one-line usage for the footer status bar, colored by usage level.
|
|
163
|
+
* The endpoint exposes reset periods (5h/7d) but not exact timestamps, so the
|
|
164
|
+
* color reflects the usage fraction rather than pace.
|
|
165
|
+
*/
|
|
166
|
+
export function formatUsageStatusColored(theme: Theme, data: UsageData): string {
|
|
167
|
+
const session = usagePercent(data.limits.session.usage);
|
|
168
|
+
const weekly = usagePercent(data.limits.weekly.usage);
|
|
169
|
+
return `${colorSegment(theme, "5h", session)} ${colorSegment(theme, "7d", weekly)}`;
|
|
170
|
+
}
|
package/utils.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
1
3
|
export async function fetchJsonWithTimeout<T>(
|
|
2
4
|
url: string,
|
|
3
5
|
init: RequestInit,
|
|
@@ -75,3 +77,37 @@ export function getContextLength(modelInfo: Record<string, unknown>): number {
|
|
|
75
77
|
}
|
|
76
78
|
return 128000;
|
|
77
79
|
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the Ollama Cloud API key for a tool execution or command.
|
|
83
|
+
*
|
|
84
|
+
* Prefers the canonical provider auth chain (ctx.modelRegistry.getApiKeyForProvider),
|
|
85
|
+
* which honors runtime/CLI key overrides, the registered
|
|
86
|
+
* apiKey: "$OLLAMA_API_KEY" config, and stored auth.json credentials. Falls back
|
|
87
|
+
* to the OLLAMA_API_KEY env var for the case where the provider is not yet
|
|
88
|
+
* registered at call time.
|
|
89
|
+
*/
|
|
90
|
+
export async function getCloudApiKey(ctx: Pick<ExtensionContext, "modelRegistry">): Promise<string | undefined> {
|
|
91
|
+
return (await ctx.modelRegistry.getApiKeyForProvider("ollama-cloud")) ?? process.env.OLLAMA_API_KEY;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Throw a user-facing error for a non-ok Ollama Cloud HTTP response, mapping
|
|
96
|
+
* distinct status codes. Shared by the web tools and the usage command.
|
|
97
|
+
*/
|
|
98
|
+
export function httpError(op: string, status: number, error?: string): never {
|
|
99
|
+
if (status === 401 || status === 403) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Ollama Cloud ${op} failed: authentication error. Check your API key in OLLAMA_API_KEY or auth.json.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (status === 429) {
|
|
105
|
+
throw new Error(`Ollama Cloud ${op} failed: rate limited. Try again shortly.`);
|
|
106
|
+
}
|
|
107
|
+
if (status >= 500) {
|
|
108
|
+
throw new Error(`Ollama Cloud ${op} failed: server error (status ${status}). Try again shortly.`);
|
|
109
|
+
}
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Ollama Cloud ${op} failed: unexpected response (status ${status}${error ? `: ${error}` : ""}). Try again shortly.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
package/web-tools.ts
CHANGED
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
import {
|
|
20
20
|
type AgentToolResult,
|
|
21
21
|
type ExtensionAPI,
|
|
22
|
-
type ExtensionContext,
|
|
23
22
|
keyHint,
|
|
24
23
|
type Theme,
|
|
25
24
|
type ToolRenderResultOptions,
|
|
@@ -28,7 +27,7 @@ import {
|
|
|
28
27
|
import { type Component, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
29
28
|
import { Type } from "@sinclair/typebox";
|
|
30
29
|
import { OLLAMA_BASE } from "./models.ts";
|
|
31
|
-
import { fetchJsonWithTimeout } from "./utils.ts";
|
|
30
|
+
import { fetchJsonWithTimeout, getCloudApiKey, httpError } from "./utils.ts";
|
|
32
31
|
|
|
33
32
|
// --- Types ---
|
|
34
33
|
|
|
@@ -50,62 +49,11 @@ interface FetchResponse {
|
|
|
50
49
|
|
|
51
50
|
const WEB_TOOLS_TIMEOUT_MS = 15000;
|
|
52
51
|
|
|
53
|
-
/**
|
|
54
|
-
* Resolve the Ollama Cloud API key for a tool execution.
|
|
55
|
-
*
|
|
56
|
-
* Prefers the canonical provider auth chain (ctx.modelRegistry.getApiKeyForProvider),
|
|
57
|
-
* which honors runtime/CLI key overrides, the registered
|
|
58
|
-
* apiKey: "$OLLAMA_API_KEY" config, and stored auth.json credentials. Falls back
|
|
59
|
-
* to the OLLAMA_API_KEY env var for the case where the provider is not yet
|
|
60
|
-
* registered at tool-call time.
|
|
61
|
-
*
|
|
62
|
-
* Exported for unit testing.
|
|
63
|
-
*/
|
|
64
|
-
export async function getCloudApiKey(ctx: Pick<ExtensionContext, "modelRegistry">): Promise<string | undefined> {
|
|
65
|
-
return (await ctx.modelRegistry.getApiKeyForProvider("ollama-cloud")) ?? process.env.OLLAMA_API_KEY;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
52
|
/** Throw a no-API-key error. */
|
|
69
53
|
function noApiKeyError(): never {
|
|
70
54
|
throw new Error("No Ollama Cloud API key configured. Set OLLAMA_API_KEY or add to auth.json.");
|
|
71
55
|
}
|
|
72
56
|
|
|
73
|
-
/** Throw a search error for a non-ok result, mapping distinct status codes. */
|
|
74
|
-
function searchError(status: number, error?: string): never {
|
|
75
|
-
if (status === 401 || status === 403) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
"Ollama Cloud search failed: authentication error. " + "Check your API key in OLLAMA_API_KEY or auth.json.",
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
if (status === 429) {
|
|
81
|
-
throw new Error("Ollama Cloud search failed: rate limited. Try again shortly.");
|
|
82
|
-
}
|
|
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
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Throw a fetch error for a non-ok result, mapping distinct status codes. */
|
|
92
|
-
function fetchError(status: number, error?: string): never {
|
|
93
|
-
if (status === 401 || status === 403) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
"Ollama Cloud fetch failed: authentication error. " + "Check your API key in OLLAMA_API_KEY or auth.json.",
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
if (status === 429) {
|
|
99
|
-
throw new Error("Ollama Cloud fetch failed: rate limited. Try again shortly.");
|
|
100
|
-
}
|
|
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
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
57
|
const PREVIEW_LINES = 8;
|
|
110
58
|
|
|
111
59
|
/**
|
|
@@ -232,7 +180,7 @@ export function registerWebSearchTool(pi: ExtensionAPI) {
|
|
|
232
180
|
);
|
|
233
181
|
|
|
234
182
|
if (!res.ok) {
|
|
235
|
-
|
|
183
|
+
httpError("search", res.status, res.error);
|
|
236
184
|
}
|
|
237
185
|
if (!isSearchResponse(res.data)) {
|
|
238
186
|
throw new Error("Web search failed: unexpected response shape from the API.");
|
|
@@ -287,7 +235,7 @@ export function registerWebFetchTool(pi: ExtensionAPI) {
|
|
|
287
235
|
);
|
|
288
236
|
|
|
289
237
|
if (!res.ok) {
|
|
290
|
-
|
|
238
|
+
httpError("fetch", res.status, res.error);
|
|
291
239
|
}
|
|
292
240
|
if (!isFetchResponse(res.data)) {
|
|
293
241
|
throw new Error("Web fetch failed: unexpected response shape from the API.");
|