pi-ollama-cloud 0.6.0 → 0.7.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 -0
- package/README.md +6 -5
- package/index.ts +2 -3
- package/models.generated.ts +66 -482
- package/models.ts +41 -2
- package/package.json +4 -2
- package/pricing.generated.ts +32 -0
- package/thinking-levels.ts +13 -0
- package/utils.ts +17 -0
- package/web-tools.ts +147 -104
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
- 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
|
+
- Restore the `OLLAMA_API_KEY` env-var fallback in `ollama_web_search` and `ollama_web_fetch`. Thanks @cawilliamson (#26).
|
|
9
|
+
- Add a 15s timeout to web search/fetch requests and preserve tool-cancellation by extending `fetchJsonWithTimeout` with an external `AbortSignal`.
|
|
10
|
+
- Always target `https://ollama.com` and warn when `OLLAMA_API_BASE` is set to a non-cloud host, instead of silently querying a local Ollama daemon. Thanks @valueforvalue (#32, #33).
|
|
11
|
+
- Add a `glm-5.2` thinking level map exposing `off`, `high`, and `xhigh` (`reasoning_effort: "none"`, `"high"`, `"max"`). Thanks @Thinkscape (#29).
|
|
12
|
+
- Add estimated per-token pricing for Ollama Cloud models so `/cost` shows comparable usage. Prices are generated from [models.dev](https://models.dev) (the same source pi uses) via `scripts/generate-pricing.ts` into `pricing.generated.ts`, not hand-typed, and regenerate with the catalog via `npm run generate-models`. Prices are pinned to the installed package version: `/ollama-cloud-refresh` updates the model list and metadata but does not re-fetch prices, so newly added models register with zero cost until the next release. Not actual subscription charges. Thanks @DxTa (#9).
|
|
13
|
+
- Refresh the generated model catalog from the live Ollama Cloud API. Adds `glm-5.2` (1M context, text-only) and `kimi-k2.7-code` (262K context, text + image). `deepseek-v4-pro` context window is now 524288 (was 1048576).
|
|
14
|
+
- Retire models deprecated per https://docs.ollama.com/cloud#deprecations: July 15, 2026 batch (`deepseek-v3.1:671b`, `deepseek-v3.2`, `devstral-2:123b`, `devstral-small-2:24b`, `ministral-3:14b`, `ministral-3:3b`, `ministral-3:8b`, `gemini-3-flash-preview`, `gemma3:12b`, `gemma3:27b`, `gemma3:4b`, `glm-4.7`, `glm-5`, `minimax-m2.1`, `qwen3-coder-next`, `qwen3-coder:480b`) and June 30 (`rnj-1:8b`). The shipped catalog goes from 30 to 18 models.
|
|
15
|
+
- Bump `@earendil-works/pi-coding-agent` runtime dependency to 0.80.10.
|
|
16
|
+
|
|
5
17
|
## [0.6.0] - 2026-06-05
|
|
6
18
|
|
|
7
19
|
- Fix `apiKey` registered as a literal string instead of an environment variable reference. Changed `apiKey: "OLLAMA_API_KEY"` to `apiKey: "$OLLAMA_API_KEY"` in `registerProvider`, resolving the deprecation warning emitted by pi v0.77.0+ and making the `OLLAMA_API_KEY` env var work alongside `auth.json` (env var takes priority, falls back to `auth.json`). Thanks @mandusm (#21).
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-ollama-cloud
|
|
2
2
|
|
|
3
|
-
Ollama Cloud provider plugin for [Pi](https://
|
|
3
|
+
Ollama Cloud provider plugin for the [Pi](https://pi.dev) coding agent.
|
|
4
4
|
|
|
5
5
|
Registers Ollama Cloud as a model provider with dynamically fetched models, and provides `ollama_web_search` and `ollama_web_fetch` tools that use the [Ollama Cloud web search API](https://docs.ollama.com/capabilities/web-search) - no local Ollama server required.
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ Registers Ollama Cloud as a model provider with dynamically fetched models, and
|
|
|
14
14
|
- **`/ollama-cloud-refresh` command** - Re-fetches the model list and updates the cache and provider registration live (no restart needed).
|
|
15
15
|
- **`ollama_web_search` tool** - Search the web for real-time information using Ollama Cloud's `/api/web_search` endpoint. Returns titles, URLs, and content snippets.
|
|
16
16
|
- **`ollama_web_fetch` tool** - Fetch and extract text content from a web page URL using Ollama Cloud's `/api/web_fetch` endpoint. Returns page title, content, and links.
|
|
17
|
-
- **
|
|
17
|
+
- **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.
|
|
18
18
|
|
|
19
19
|
## Prerequisites
|
|
20
20
|
|
|
@@ -142,11 +142,11 @@ Model metadata is derived from the cached data:
|
|
|
142
142
|
| Field | Source |
|
|
143
143
|
|---|---|
|
|
144
144
|
| `reasoning` | `capabilities` includes `"thinking"` |
|
|
145
|
-
| `thinkingLevelMap` | [`thinking-levels.ts`](thinking-levels.ts) with
|
|
145
|
+
| `thinkingLevelMap` | [`thinking-levels.ts`](thinking-levels.ts) with 5 maps (DEFAULT, GPT_OSS, QWEN3, GLM_52, NO_OFF) based on API testing |
|
|
146
146
|
| `input` | `["text", "image"]` if `capabilities` includes `"vision"`, else `["text"]` |
|
|
147
147
|
| `contextWindow` | `model_info.*.context_length` (falls back to 128000) |
|
|
148
148
|
| `maxTokens` | Fixed at 32768 |
|
|
149
|
-
| `cost` |
|
|
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: `/ollama-cloud-refresh` updates the model list and metadata but does not re-fetch prices, so newly added models register with zero cost until the next release. |
|
|
150
150
|
|
|
151
151
|
### Thinking level mapping
|
|
152
152
|
|
|
@@ -157,6 +157,7 @@ Pi's thinking levels are mapped to Ollama Cloud's OpenAI-compatible `reasoning_e
|
|
|
157
157
|
| `DEFAULT` | Most thinking models | off, low, medium, high, xhigh | `minimal` hidden (duplicate of low) |
|
|
158
158
|
| `GPT_OSS` | `gpt-oss*` | low, medium, high | Can't disable thinking, no off or xhigh |
|
|
159
159
|
| `QWEN3` | `qwen3*` (except `qwen3-vl*`) | off, medium | Binary-only (think/nothink), no gradation |
|
|
160
|
+
| `GLM_52` | `glm-5.2` | off, high, xhigh | GLM supports disabled thinking; Ollama's model page confirms `high` and `max` reasoning efforts |
|
|
160
161
|
| `NO_OFF` | `qwen3-vl*`, `kimi-k2-thinking`, `minimax*` | low, medium, high, xhigh | "none" doesn't disable thinking on these models |
|
|
161
162
|
|
|
162
163
|
See [docs/think-experiment.md](docs/think-experiment.md) for the testing methodology and results.
|
|
@@ -182,7 +183,7 @@ Both tools use the same Ollama Cloud API key configured for the provider. No loc
|
|
|
182
183
|
|
|
183
184
|
| Command | Description |
|
|
184
185
|
|---|---|
|
|
185
|
-
| `/ollama-cloud-refresh` | Fetch models from the Ollama Cloud API, update cache, and re-register the provider |
|
|
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). |
|
|
186
187
|
| `/ollama-webtools [on\|off\|enable\|disable]` | Enable or disable the `ollama_web_search` and `ollama_web_fetch` tools. Toggles if no argument given. |
|
|
187
188
|
|
|
188
189
|
## Development
|
package/index.ts
CHANGED
|
@@ -124,9 +124,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
124
124
|
// When cache is missing, GENERATED_MODELS serves as the cache —
|
|
125
125
|
// it is manually generated via `npm run generate-models` and committed to the repo.
|
|
126
126
|
const needsStartupRefresh = cacheState.status === "stale";
|
|
127
|
-
// GENERATED_MODELS ships with the package
|
|
128
|
-
//
|
|
129
|
-
// from /ollama-cloud-refresh takes precedence over the generated list.
|
|
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.
|
|
130
129
|
const models = cacheState.status === "missing" ? GENERATED_MODELS : assembleModels(cacheState.models);
|
|
131
130
|
|
|
132
131
|
registerProvider(pi, models);
|
package/models.generated.ts
CHANGED
|
@@ -1,91 +1,11 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-models.ts
|
|
2
2
|
// Do not edit manually.
|
|
3
|
-
// Generated: 2026-
|
|
4
|
-
// Model count:
|
|
3
|
+
// Generated: 2026-07-17T19:55:27.551Z
|
|
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
|
-
{
|
|
10
|
-
id: "deepseek-v3.1:671b",
|
|
11
|
-
name: "deepseek-v3.1:671b",
|
|
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: 163840,
|
|
31
|
-
cost: {
|
|
32
|
-
cacheRead: 0,
|
|
33
|
-
cacheWrite: 0,
|
|
34
|
-
input: 0,
|
|
35
|
-
output: 0,
|
|
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-v3.2",
|
|
51
|
-
name: "deepseek-v3.2",
|
|
52
|
-
compat: {
|
|
53
|
-
maxTokensField: "max_tokens",
|
|
54
|
-
openRouterRouting: {},
|
|
55
|
-
requiresAssistantAfterToolResult: false,
|
|
56
|
-
requiresReasoningContentOnAssistantMessages: false,
|
|
57
|
-
requiresThinkingAsText: false,
|
|
58
|
-
requiresToolResultName: false,
|
|
59
|
-
sendSessionAffinityHeaders: false,
|
|
60
|
-
supportsDeveloperRole: false,
|
|
61
|
-
supportsLongCacheRetention: false,
|
|
62
|
-
supportsReasoningEffort: true,
|
|
63
|
-
supportsStore: false,
|
|
64
|
-
supportsStrictMode: false,
|
|
65
|
-
supportsUsageInStreaming: true,
|
|
66
|
-
thinkingFormat: "openai",
|
|
67
|
-
vercelGatewayRouting: {},
|
|
68
|
-
zaiToolStream: false,
|
|
69
|
-
},
|
|
70
|
-
contextWindow: 163840,
|
|
71
|
-
cost: {
|
|
72
|
-
cacheRead: 0,
|
|
73
|
-
cacheWrite: 0,
|
|
74
|
-
input: 0,
|
|
75
|
-
output: 0,
|
|
76
|
-
},
|
|
77
|
-
input: ["text"],
|
|
78
|
-
maxTokens: 32768,
|
|
79
|
-
reasoning: true,
|
|
80
|
-
thinkingLevelMap: {
|
|
81
|
-
high: "high",
|
|
82
|
-
low: "low",
|
|
83
|
-
medium: "medium",
|
|
84
|
-
minimal: null,
|
|
85
|
-
off: "none",
|
|
86
|
-
xhigh: "max",
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
9
|
{
|
|
90
10
|
id: "deepseek-v4-flash",
|
|
91
11
|
name: "deepseek-v4-flash",
|
|
@@ -109,10 +29,10 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
109
29
|
},
|
|
110
30
|
contextWindow: 1048576,
|
|
111
31
|
cost: {
|
|
112
|
-
cacheRead: 0,
|
|
32
|
+
cacheRead: 0.0028,
|
|
113
33
|
cacheWrite: 0,
|
|
114
|
-
input: 0,
|
|
115
|
-
output: 0,
|
|
34
|
+
input: 0.14,
|
|
35
|
+
output: 0.28,
|
|
116
36
|
},
|
|
117
37
|
input: ["text"],
|
|
118
38
|
maxTokens: 32768,
|
|
@@ -147,119 +67,15 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
147
67
|
vercelGatewayRouting: {},
|
|
148
68
|
zaiToolStream: false,
|
|
149
69
|
},
|
|
150
|
-
contextWindow:
|
|
151
|
-
cost: {
|
|
152
|
-
cacheRead: 0,
|
|
153
|
-
cacheWrite: 0,
|
|
154
|
-
input: 0,
|
|
155
|
-
output: 0,
|
|
156
|
-
},
|
|
157
|
-
input: ["text"],
|
|
158
|
-
maxTokens: 32768,
|
|
159
|
-
reasoning: true,
|
|
160
|
-
thinkingLevelMap: {
|
|
161
|
-
high: "high",
|
|
162
|
-
low: "low",
|
|
163
|
-
medium: "medium",
|
|
164
|
-
minimal: null,
|
|
165
|
-
off: "none",
|
|
166
|
-
xhigh: "max",
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
id: "devstral-2:123b",
|
|
171
|
-
name: "devstral-2:123b",
|
|
172
|
-
compat: {
|
|
173
|
-
maxTokensField: "max_tokens",
|
|
174
|
-
openRouterRouting: {},
|
|
175
|
-
requiresAssistantAfterToolResult: false,
|
|
176
|
-
requiresReasoningContentOnAssistantMessages: false,
|
|
177
|
-
requiresThinkingAsText: false,
|
|
178
|
-
requiresToolResultName: false,
|
|
179
|
-
sendSessionAffinityHeaders: false,
|
|
180
|
-
supportsDeveloperRole: false,
|
|
181
|
-
supportsLongCacheRetention: false,
|
|
182
|
-
supportsReasoningEffort: true,
|
|
183
|
-
supportsStore: false,
|
|
184
|
-
supportsStrictMode: false,
|
|
185
|
-
supportsUsageInStreaming: true,
|
|
186
|
-
thinkingFormat: "openai",
|
|
187
|
-
vercelGatewayRouting: {},
|
|
188
|
-
zaiToolStream: false,
|
|
189
|
-
},
|
|
190
|
-
contextWindow: 262144,
|
|
70
|
+
contextWindow: 524288,
|
|
191
71
|
cost: {
|
|
192
|
-
cacheRead: 0,
|
|
72
|
+
cacheRead: 0.003625,
|
|
193
73
|
cacheWrite: 0,
|
|
194
|
-
input: 0,
|
|
195
|
-
output: 0,
|
|
74
|
+
input: 0.435,
|
|
75
|
+
output: 0.87,
|
|
196
76
|
},
|
|
197
77
|
input: ["text"],
|
|
198
78
|
maxTokens: 32768,
|
|
199
|
-
reasoning: false,
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
id: "devstral-small-2:24b",
|
|
203
|
-
name: "devstral-small-2:24b",
|
|
204
|
-
compat: {
|
|
205
|
-
maxTokensField: "max_tokens",
|
|
206
|
-
openRouterRouting: {},
|
|
207
|
-
requiresAssistantAfterToolResult: false,
|
|
208
|
-
requiresReasoningContentOnAssistantMessages: false,
|
|
209
|
-
requiresThinkingAsText: false,
|
|
210
|
-
requiresToolResultName: false,
|
|
211
|
-
sendSessionAffinityHeaders: false,
|
|
212
|
-
supportsDeveloperRole: false,
|
|
213
|
-
supportsLongCacheRetention: false,
|
|
214
|
-
supportsReasoningEffort: true,
|
|
215
|
-
supportsStore: false,
|
|
216
|
-
supportsStrictMode: false,
|
|
217
|
-
supportsUsageInStreaming: true,
|
|
218
|
-
thinkingFormat: "openai",
|
|
219
|
-
vercelGatewayRouting: {},
|
|
220
|
-
zaiToolStream: false,
|
|
221
|
-
},
|
|
222
|
-
contextWindow: 262144,
|
|
223
|
-
cost: {
|
|
224
|
-
cacheRead: 0,
|
|
225
|
-
cacheWrite: 0,
|
|
226
|
-
input: 0,
|
|
227
|
-
output: 0,
|
|
228
|
-
},
|
|
229
|
-
input: ["text", "image"],
|
|
230
|
-
maxTokens: 32768,
|
|
231
|
-
reasoning: false,
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
id: "gemini-3-flash-preview",
|
|
235
|
-
name: "gemini-3-flash-preview",
|
|
236
|
-
compat: {
|
|
237
|
-
maxTokensField: "max_tokens",
|
|
238
|
-
openRouterRouting: {},
|
|
239
|
-
requiresAssistantAfterToolResult: false,
|
|
240
|
-
requiresReasoningContentOnAssistantMessages: false,
|
|
241
|
-
requiresThinkingAsText: false,
|
|
242
|
-
requiresToolResultName: false,
|
|
243
|
-
sendSessionAffinityHeaders: false,
|
|
244
|
-
supportsDeveloperRole: false,
|
|
245
|
-
supportsLongCacheRetention: false,
|
|
246
|
-
supportsReasoningEffort: true,
|
|
247
|
-
supportsStore: false,
|
|
248
|
-
supportsStrictMode: false,
|
|
249
|
-
supportsUsageInStreaming: true,
|
|
250
|
-
thinkingFormat: "openai",
|
|
251
|
-
vercelGatewayRouting: {},
|
|
252
|
-
zaiToolStream: false,
|
|
253
|
-
},
|
|
254
|
-
contextWindow: 1048576,
|
|
255
|
-
cost: {
|
|
256
|
-
cacheRead: 0,
|
|
257
|
-
cacheWrite: 0,
|
|
258
|
-
input: 0,
|
|
259
|
-
output: 0,
|
|
260
|
-
},
|
|
261
|
-
input: ["text", "image"],
|
|
262
|
-
maxTokens: 32768,
|
|
263
79
|
reasoning: true,
|
|
264
80
|
thinkingLevelMap: {
|
|
265
81
|
high: "high",
|
|
@@ -293,10 +109,10 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
293
109
|
},
|
|
294
110
|
contextWindow: 262144,
|
|
295
111
|
cost: {
|
|
296
|
-
cacheRead: 0,
|
|
112
|
+
cacheRead: 0.12,
|
|
297
113
|
cacheWrite: 0,
|
|
298
|
-
input: 0,
|
|
299
|
-
output: 0,
|
|
114
|
+
input: 0.22,
|
|
115
|
+
output: 0.55,
|
|
300
116
|
},
|
|
301
117
|
input: ["text", "image"],
|
|
302
118
|
maxTokens: 32768,
|
|
@@ -311,48 +127,8 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
311
127
|
},
|
|
312
128
|
},
|
|
313
129
|
{
|
|
314
|
-
id: "glm-
|
|
315
|
-
name: "glm-
|
|
316
|
-
compat: {
|
|
317
|
-
maxTokensField: "max_tokens",
|
|
318
|
-
openRouterRouting: {},
|
|
319
|
-
requiresAssistantAfterToolResult: false,
|
|
320
|
-
requiresReasoningContentOnAssistantMessages: false,
|
|
321
|
-
requiresThinkingAsText: false,
|
|
322
|
-
requiresToolResultName: false,
|
|
323
|
-
sendSessionAffinityHeaders: false,
|
|
324
|
-
supportsDeveloperRole: false,
|
|
325
|
-
supportsLongCacheRetention: false,
|
|
326
|
-
supportsReasoningEffort: true,
|
|
327
|
-
supportsStore: false,
|
|
328
|
-
supportsStrictMode: false,
|
|
329
|
-
supportsUsageInStreaming: true,
|
|
330
|
-
thinkingFormat: "openai",
|
|
331
|
-
vercelGatewayRouting: {},
|
|
332
|
-
zaiToolStream: false,
|
|
333
|
-
},
|
|
334
|
-
contextWindow: 202752,
|
|
335
|
-
cost: {
|
|
336
|
-
cacheRead: 0,
|
|
337
|
-
cacheWrite: 0,
|
|
338
|
-
input: 0,
|
|
339
|
-
output: 0,
|
|
340
|
-
},
|
|
341
|
-
input: ["text"],
|
|
342
|
-
maxTokens: 32768,
|
|
343
|
-
reasoning: true,
|
|
344
|
-
thinkingLevelMap: {
|
|
345
|
-
high: "high",
|
|
346
|
-
low: "low",
|
|
347
|
-
medium: "medium",
|
|
348
|
-
minimal: null,
|
|
349
|
-
off: "none",
|
|
350
|
-
xhigh: "max",
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
id: "glm-5",
|
|
355
|
-
name: "glm-5",
|
|
130
|
+
id: "glm-5.1",
|
|
131
|
+
name: "glm-5.1",
|
|
356
132
|
compat: {
|
|
357
133
|
maxTokensField: "max_tokens",
|
|
358
134
|
openRouterRouting: {},
|
|
@@ -373,10 +149,10 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
373
149
|
},
|
|
374
150
|
contextWindow: 202752,
|
|
375
151
|
cost: {
|
|
376
|
-
cacheRead: 0,
|
|
152
|
+
cacheRead: 0.26,
|
|
377
153
|
cacheWrite: 0,
|
|
378
|
-
input:
|
|
379
|
-
output:
|
|
154
|
+
input: 1.4,
|
|
155
|
+
output: 4.4,
|
|
380
156
|
},
|
|
381
157
|
input: ["text"],
|
|
382
158
|
maxTokens: 32768,
|
|
@@ -391,8 +167,8 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
391
167
|
},
|
|
392
168
|
},
|
|
393
169
|
{
|
|
394
|
-
id: "glm-5.
|
|
395
|
-
name: "glm-5.
|
|
170
|
+
id: "glm-5.2",
|
|
171
|
+
name: "glm-5.2",
|
|
396
172
|
compat: {
|
|
397
173
|
maxTokensField: "max_tokens",
|
|
398
174
|
openRouterRouting: {},
|
|
@@ -411,20 +187,20 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
411
187
|
vercelGatewayRouting: {},
|
|
412
188
|
zaiToolStream: false,
|
|
413
189
|
},
|
|
414
|
-
contextWindow:
|
|
190
|
+
contextWindow: 1000000,
|
|
415
191
|
cost: {
|
|
416
|
-
cacheRead: 0,
|
|
192
|
+
cacheRead: 0.26,
|
|
417
193
|
cacheWrite: 0,
|
|
418
|
-
input:
|
|
419
|
-
output:
|
|
194
|
+
input: 1.4,
|
|
195
|
+
output: 4.4,
|
|
420
196
|
},
|
|
421
197
|
input: ["text"],
|
|
422
198
|
maxTokens: 32768,
|
|
423
199
|
reasoning: true,
|
|
424
200
|
thinkingLevelMap: {
|
|
425
201
|
high: "high",
|
|
426
|
-
low:
|
|
427
|
-
medium:
|
|
202
|
+
low: null,
|
|
203
|
+
medium: null,
|
|
428
204
|
minimal: null,
|
|
429
205
|
off: "none",
|
|
430
206
|
xhigh: "max",
|
|
@@ -455,8 +231,8 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
455
231
|
cost: {
|
|
456
232
|
cacheRead: 0,
|
|
457
233
|
cacheWrite: 0,
|
|
458
|
-
input: 0,
|
|
459
|
-
output: 0,
|
|
234
|
+
input: 0.037,
|
|
235
|
+
output: 0.17,
|
|
460
236
|
},
|
|
461
237
|
input: ["text"],
|
|
462
238
|
maxTokens: 32768,
|
|
@@ -493,10 +269,10 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
493
269
|
},
|
|
494
270
|
contextWindow: 131072,
|
|
495
271
|
cost: {
|
|
496
|
-
cacheRead: 0,
|
|
272
|
+
cacheRead: 0.03,
|
|
497
273
|
cacheWrite: 0,
|
|
498
|
-
input: 0,
|
|
499
|
-
output: 0,
|
|
274
|
+
input: 0.03,
|
|
275
|
+
output: 0.13,
|
|
500
276
|
},
|
|
501
277
|
input: ["text"],
|
|
502
278
|
maxTokens: 32768,
|
|
@@ -533,10 +309,10 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
533
309
|
},
|
|
534
310
|
contextWindow: 262144,
|
|
535
311
|
cost: {
|
|
536
|
-
cacheRead: 0,
|
|
312
|
+
cacheRead: 0.1,
|
|
537
313
|
cacheWrite: 0,
|
|
538
|
-
input: 0,
|
|
539
|
-
output:
|
|
314
|
+
input: 0.6,
|
|
315
|
+
output: 3,
|
|
540
316
|
},
|
|
541
317
|
input: ["text", "image"],
|
|
542
318
|
maxTokens: 32768,
|
|
@@ -573,10 +349,10 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
573
349
|
},
|
|
574
350
|
contextWindow: 262144,
|
|
575
351
|
cost: {
|
|
576
|
-
cacheRead: 0,
|
|
352
|
+
cacheRead: 0.16,
|
|
577
353
|
cacheWrite: 0,
|
|
578
|
-
input: 0,
|
|
579
|
-
output:
|
|
354
|
+
input: 0.95,
|
|
355
|
+
output: 4,
|
|
580
356
|
},
|
|
581
357
|
input: ["text", "image"],
|
|
582
358
|
maxTokens: 32768,
|
|
@@ -591,8 +367,8 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
591
367
|
},
|
|
592
368
|
},
|
|
593
369
|
{
|
|
594
|
-
id: "
|
|
595
|
-
name: "
|
|
370
|
+
id: "kimi-k2.7-code",
|
|
371
|
+
name: "kimi-k2.7-code",
|
|
596
372
|
compat: {
|
|
597
373
|
maxTokensField: "max_tokens",
|
|
598
374
|
openRouterRouting: {},
|
|
@@ -611,14 +387,14 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
611
387
|
vercelGatewayRouting: {},
|
|
612
388
|
zaiToolStream: false,
|
|
613
389
|
},
|
|
614
|
-
contextWindow:
|
|
390
|
+
contextWindow: 262144,
|
|
615
391
|
cost: {
|
|
616
|
-
cacheRead: 0,
|
|
392
|
+
cacheRead: 0.19,
|
|
617
393
|
cacheWrite: 0,
|
|
618
|
-
input: 0,
|
|
619
|
-
output:
|
|
394
|
+
input: 0.95,
|
|
395
|
+
output: 4,
|
|
620
396
|
},
|
|
621
|
-
input: ["text"],
|
|
397
|
+
input: ["text", "image"],
|
|
622
398
|
maxTokens: 32768,
|
|
623
399
|
reasoning: true,
|
|
624
400
|
thinkingLevelMap: {
|
|
@@ -626,7 +402,7 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
626
402
|
low: "low",
|
|
627
403
|
medium: "medium",
|
|
628
404
|
minimal: null,
|
|
629
|
-
off:
|
|
405
|
+
off: "none",
|
|
630
406
|
xhigh: "max",
|
|
631
407
|
},
|
|
632
408
|
},
|
|
@@ -653,10 +429,10 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
653
429
|
},
|
|
654
430
|
contextWindow: 196608,
|
|
655
431
|
cost: {
|
|
656
|
-
cacheRead: 0,
|
|
657
|
-
cacheWrite: 0,
|
|
658
|
-
input: 0,
|
|
659
|
-
output:
|
|
432
|
+
cacheRead: 0.03,
|
|
433
|
+
cacheWrite: 0.375,
|
|
434
|
+
input: 0.3,
|
|
435
|
+
output: 1.2,
|
|
660
436
|
},
|
|
661
437
|
input: ["text"],
|
|
662
438
|
maxTokens: 32768,
|
|
@@ -693,10 +469,10 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
693
469
|
},
|
|
694
470
|
contextWindow: 196608,
|
|
695
471
|
cost: {
|
|
696
|
-
cacheRead: 0,
|
|
697
|
-
cacheWrite: 0,
|
|
698
|
-
input: 0,
|
|
699
|
-
output:
|
|
472
|
+
cacheRead: 0.06,
|
|
473
|
+
cacheWrite: 0.375,
|
|
474
|
+
input: 0.3,
|
|
475
|
+
output: 1.2,
|
|
700
476
|
},
|
|
701
477
|
input: ["text"],
|
|
702
478
|
maxTokens: 32768,
|
|
@@ -733,10 +509,10 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
733
509
|
},
|
|
734
510
|
contextWindow: 524288,
|
|
735
511
|
cost: {
|
|
736
|
-
cacheRead: 0,
|
|
512
|
+
cacheRead: 0.06,
|
|
737
513
|
cacheWrite: 0,
|
|
738
|
-
input: 0,
|
|
739
|
-
output:
|
|
514
|
+
input: 0.3,
|
|
515
|
+
output: 1.2,
|
|
740
516
|
},
|
|
741
517
|
input: ["text", "image"],
|
|
742
518
|
maxTokens: 32768,
|
|
@@ -750,102 +526,6 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
750
526
|
xhigh: "max",
|
|
751
527
|
},
|
|
752
528
|
},
|
|
753
|
-
{
|
|
754
|
-
id: "ministral-3:14b",
|
|
755
|
-
name: "ministral-3:14b",
|
|
756
|
-
compat: {
|
|
757
|
-
maxTokensField: "max_tokens",
|
|
758
|
-
openRouterRouting: {},
|
|
759
|
-
requiresAssistantAfterToolResult: false,
|
|
760
|
-
requiresReasoningContentOnAssistantMessages: false,
|
|
761
|
-
requiresThinkingAsText: false,
|
|
762
|
-
requiresToolResultName: false,
|
|
763
|
-
sendSessionAffinityHeaders: false,
|
|
764
|
-
supportsDeveloperRole: false,
|
|
765
|
-
supportsLongCacheRetention: false,
|
|
766
|
-
supportsReasoningEffort: true,
|
|
767
|
-
supportsStore: false,
|
|
768
|
-
supportsStrictMode: false,
|
|
769
|
-
supportsUsageInStreaming: true,
|
|
770
|
-
thinkingFormat: "openai",
|
|
771
|
-
vercelGatewayRouting: {},
|
|
772
|
-
zaiToolStream: false,
|
|
773
|
-
},
|
|
774
|
-
contextWindow: 262144,
|
|
775
|
-
cost: {
|
|
776
|
-
cacheRead: 0,
|
|
777
|
-
cacheWrite: 0,
|
|
778
|
-
input: 0,
|
|
779
|
-
output: 0,
|
|
780
|
-
},
|
|
781
|
-
input: ["text", "image"],
|
|
782
|
-
maxTokens: 32768,
|
|
783
|
-
reasoning: false,
|
|
784
|
-
},
|
|
785
|
-
{
|
|
786
|
-
id: "ministral-3:3b",
|
|
787
|
-
name: "ministral-3:3b",
|
|
788
|
-
compat: {
|
|
789
|
-
maxTokensField: "max_tokens",
|
|
790
|
-
openRouterRouting: {},
|
|
791
|
-
requiresAssistantAfterToolResult: false,
|
|
792
|
-
requiresReasoningContentOnAssistantMessages: false,
|
|
793
|
-
requiresThinkingAsText: false,
|
|
794
|
-
requiresToolResultName: false,
|
|
795
|
-
sendSessionAffinityHeaders: false,
|
|
796
|
-
supportsDeveloperRole: false,
|
|
797
|
-
supportsLongCacheRetention: false,
|
|
798
|
-
supportsReasoningEffort: true,
|
|
799
|
-
supportsStore: false,
|
|
800
|
-
supportsStrictMode: false,
|
|
801
|
-
supportsUsageInStreaming: true,
|
|
802
|
-
thinkingFormat: "openai",
|
|
803
|
-
vercelGatewayRouting: {},
|
|
804
|
-
zaiToolStream: false,
|
|
805
|
-
},
|
|
806
|
-
contextWindow: 262144,
|
|
807
|
-
cost: {
|
|
808
|
-
cacheRead: 0,
|
|
809
|
-
cacheWrite: 0,
|
|
810
|
-
input: 0,
|
|
811
|
-
output: 0,
|
|
812
|
-
},
|
|
813
|
-
input: ["text", "image"],
|
|
814
|
-
maxTokens: 32768,
|
|
815
|
-
reasoning: false,
|
|
816
|
-
},
|
|
817
|
-
{
|
|
818
|
-
id: "ministral-3:8b",
|
|
819
|
-
name: "ministral-3:8b",
|
|
820
|
-
compat: {
|
|
821
|
-
maxTokensField: "max_tokens",
|
|
822
|
-
openRouterRouting: {},
|
|
823
|
-
requiresAssistantAfterToolResult: false,
|
|
824
|
-
requiresReasoningContentOnAssistantMessages: false,
|
|
825
|
-
requiresThinkingAsText: false,
|
|
826
|
-
requiresToolResultName: false,
|
|
827
|
-
sendSessionAffinityHeaders: false,
|
|
828
|
-
supportsDeveloperRole: false,
|
|
829
|
-
supportsLongCacheRetention: false,
|
|
830
|
-
supportsReasoningEffort: true,
|
|
831
|
-
supportsStore: false,
|
|
832
|
-
supportsStrictMode: false,
|
|
833
|
-
supportsUsageInStreaming: true,
|
|
834
|
-
thinkingFormat: "openai",
|
|
835
|
-
vercelGatewayRouting: {},
|
|
836
|
-
zaiToolStream: false,
|
|
837
|
-
},
|
|
838
|
-
contextWindow: 262144,
|
|
839
|
-
cost: {
|
|
840
|
-
cacheRead: 0,
|
|
841
|
-
cacheWrite: 0,
|
|
842
|
-
input: 0,
|
|
843
|
-
output: 0,
|
|
844
|
-
},
|
|
845
|
-
input: ["text", "image"],
|
|
846
|
-
maxTokens: 32768,
|
|
847
|
-
reasoning: false,
|
|
848
|
-
},
|
|
849
529
|
{
|
|
850
530
|
id: "mistral-large-3:675b",
|
|
851
531
|
name: "mistral-large-3:675b",
|
|
@@ -871,8 +551,8 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
871
551
|
cost: {
|
|
872
552
|
cacheRead: 0,
|
|
873
553
|
cacheWrite: 0,
|
|
874
|
-
input: 0,
|
|
875
|
-
output:
|
|
554
|
+
input: 0.5,
|
|
555
|
+
output: 1.5,
|
|
876
556
|
},
|
|
877
557
|
input: ["text", "image"],
|
|
878
558
|
maxTokens: 32768,
|
|
@@ -903,8 +583,8 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
903
583
|
cost: {
|
|
904
584
|
cacheRead: 0,
|
|
905
585
|
cacheWrite: 0,
|
|
906
|
-
input: 0,
|
|
907
|
-
output: 0,
|
|
586
|
+
input: 0.05,
|
|
587
|
+
output: 0.2,
|
|
908
588
|
},
|
|
909
589
|
input: ["text"],
|
|
910
590
|
maxTokens: 32768,
|
|
@@ -943,8 +623,8 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
943
623
|
cost: {
|
|
944
624
|
cacheRead: 0,
|
|
945
625
|
cacheWrite: 0,
|
|
946
|
-
input: 0,
|
|
947
|
-
output: 0,
|
|
626
|
+
input: 0.2,
|
|
627
|
+
output: 0.8,
|
|
948
628
|
},
|
|
949
629
|
input: ["text"],
|
|
950
630
|
maxTokens: 32768,
|
|
@@ -981,10 +661,10 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
981
661
|
},
|
|
982
662
|
contextWindow: 262144,
|
|
983
663
|
cost: {
|
|
984
|
-
cacheRead: 0,
|
|
664
|
+
cacheRead: 0.15,
|
|
985
665
|
cacheWrite: 0,
|
|
986
|
-
input: 0,
|
|
987
|
-
output:
|
|
666
|
+
input: 0.5,
|
|
667
|
+
output: 2.5,
|
|
988
668
|
},
|
|
989
669
|
input: ["text"],
|
|
990
670
|
maxTokens: 32768,
|
|
@@ -998,70 +678,6 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
998
678
|
xhigh: "max",
|
|
999
679
|
},
|
|
1000
680
|
},
|
|
1001
|
-
{
|
|
1002
|
-
id: "qwen3-coder-next",
|
|
1003
|
-
name: "qwen3-coder-next",
|
|
1004
|
-
compat: {
|
|
1005
|
-
maxTokensField: "max_tokens",
|
|
1006
|
-
openRouterRouting: {},
|
|
1007
|
-
requiresAssistantAfterToolResult: false,
|
|
1008
|
-
requiresReasoningContentOnAssistantMessages: false,
|
|
1009
|
-
requiresThinkingAsText: false,
|
|
1010
|
-
requiresToolResultName: false,
|
|
1011
|
-
sendSessionAffinityHeaders: false,
|
|
1012
|
-
supportsDeveloperRole: false,
|
|
1013
|
-
supportsLongCacheRetention: false,
|
|
1014
|
-
supportsReasoningEffort: true,
|
|
1015
|
-
supportsStore: false,
|
|
1016
|
-
supportsStrictMode: false,
|
|
1017
|
-
supportsUsageInStreaming: true,
|
|
1018
|
-
thinkingFormat: "openai",
|
|
1019
|
-
vercelGatewayRouting: {},
|
|
1020
|
-
zaiToolStream: false,
|
|
1021
|
-
},
|
|
1022
|
-
contextWindow: 262144,
|
|
1023
|
-
cost: {
|
|
1024
|
-
cacheRead: 0,
|
|
1025
|
-
cacheWrite: 0,
|
|
1026
|
-
input: 0,
|
|
1027
|
-
output: 0,
|
|
1028
|
-
},
|
|
1029
|
-
input: ["text"],
|
|
1030
|
-
maxTokens: 32768,
|
|
1031
|
-
reasoning: false,
|
|
1032
|
-
},
|
|
1033
|
-
{
|
|
1034
|
-
id: "qwen3-coder:480b",
|
|
1035
|
-
name: "qwen3-coder:480b",
|
|
1036
|
-
compat: {
|
|
1037
|
-
maxTokensField: "max_tokens",
|
|
1038
|
-
openRouterRouting: {},
|
|
1039
|
-
requiresAssistantAfterToolResult: false,
|
|
1040
|
-
requiresReasoningContentOnAssistantMessages: false,
|
|
1041
|
-
requiresThinkingAsText: false,
|
|
1042
|
-
requiresToolResultName: false,
|
|
1043
|
-
sendSessionAffinityHeaders: false,
|
|
1044
|
-
supportsDeveloperRole: false,
|
|
1045
|
-
supportsLongCacheRetention: false,
|
|
1046
|
-
supportsReasoningEffort: true,
|
|
1047
|
-
supportsStore: false,
|
|
1048
|
-
supportsStrictMode: false,
|
|
1049
|
-
supportsUsageInStreaming: true,
|
|
1050
|
-
thinkingFormat: "openai",
|
|
1051
|
-
vercelGatewayRouting: {},
|
|
1052
|
-
zaiToolStream: false,
|
|
1053
|
-
},
|
|
1054
|
-
contextWindow: 262144,
|
|
1055
|
-
cost: {
|
|
1056
|
-
cacheRead: 0,
|
|
1057
|
-
cacheWrite: 0,
|
|
1058
|
-
input: 0,
|
|
1059
|
-
output: 0,
|
|
1060
|
-
},
|
|
1061
|
-
input: ["text"],
|
|
1062
|
-
maxTokens: 32768,
|
|
1063
|
-
reasoning: false,
|
|
1064
|
-
},
|
|
1065
681
|
{
|
|
1066
682
|
id: "qwen3.5:397b",
|
|
1067
683
|
name: "qwen3.5:397b",
|
|
@@ -1087,8 +703,8 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
1087
703
|
cost: {
|
|
1088
704
|
cacheRead: 0,
|
|
1089
705
|
cacheWrite: 0,
|
|
1090
|
-
input: 0,
|
|
1091
|
-
output:
|
|
706
|
+
input: 0.6,
|
|
707
|
+
output: 3.6,
|
|
1092
708
|
},
|
|
1093
709
|
input: ["text", "image"],
|
|
1094
710
|
maxTokens: 32768,
|
|
@@ -1102,36 +718,4 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
|
|
|
1102
718
|
xhigh: null,
|
|
1103
719
|
},
|
|
1104
720
|
},
|
|
1105
|
-
{
|
|
1106
|
-
id: "rnj-1:8b",
|
|
1107
|
-
name: "rnj-1:8b",
|
|
1108
|
-
compat: {
|
|
1109
|
-
maxTokensField: "max_tokens",
|
|
1110
|
-
openRouterRouting: {},
|
|
1111
|
-
requiresAssistantAfterToolResult: false,
|
|
1112
|
-
requiresReasoningContentOnAssistantMessages: false,
|
|
1113
|
-
requiresThinkingAsText: false,
|
|
1114
|
-
requiresToolResultName: false,
|
|
1115
|
-
sendSessionAffinityHeaders: false,
|
|
1116
|
-
supportsDeveloperRole: false,
|
|
1117
|
-
supportsLongCacheRetention: false,
|
|
1118
|
-
supportsReasoningEffort: true,
|
|
1119
|
-
supportsStore: false,
|
|
1120
|
-
supportsStrictMode: false,
|
|
1121
|
-
supportsUsageInStreaming: true,
|
|
1122
|
-
thinkingFormat: "openai",
|
|
1123
|
-
vercelGatewayRouting: {},
|
|
1124
|
-
zaiToolStream: false,
|
|
1125
|
-
},
|
|
1126
|
-
contextWindow: 32768,
|
|
1127
|
-
cost: {
|
|
1128
|
-
cacheRead: 0,
|
|
1129
|
-
cacheWrite: 0,
|
|
1130
|
-
input: 0,
|
|
1131
|
-
output: 0,
|
|
1132
|
-
},
|
|
1133
|
-
input: ["text"],
|
|
1134
|
-
maxTokens: 32768,
|
|
1135
|
-
reasoning: false,
|
|
1136
|
-
},
|
|
1137
721
|
];
|
package/models.ts
CHANGED
|
@@ -1,16 +1,55 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { type ExtensionCommandContext, getAgentDir, type ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { MODEL_PRICING, type ModelPrice } from "./pricing.generated.ts";
|
|
4
5
|
import { resolve as resolveThinkingLevelMap } from "./thinking-levels.ts";
|
|
5
6
|
import { concurrentMap, fetchJsonWithTimeout, getContextLength } from "./utils.ts";
|
|
6
7
|
|
|
8
|
+
// --- Pricing ---
|
|
9
|
+
// Estimated per-1M-token prices are generated from models.dev by
|
|
10
|
+
// scripts/generate-pricing.ts (see pricing.generated.ts, do not edit by hand).
|
|
11
|
+
// Ollama Cloud is subscription-billed; these are equivalent pay-as-you-go
|
|
12
|
+
// estimates so /cost shows comparable usage, not actual charges.
|
|
13
|
+
|
|
14
|
+
/** Resolve the estimated price for an Ollama Cloud model ID. Exact match only;
|
|
15
|
+
* unmapped models return zero. */
|
|
16
|
+
function resolvePrice(id: string): ModelPrice {
|
|
17
|
+
return MODEL_PRICING[id] ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
// --- Constants ---
|
|
8
21
|
const CACHE_DIR = join(getAgentDir(), "cache");
|
|
9
22
|
const CACHE_FILE = join(CACHE_DIR, "ollama-cloud-models.json");
|
|
10
23
|
const CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
11
24
|
const FETCH_TIMEOUT_MS = 10000;
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
// The cloud extension always targets ollama.com; local Ollama daemons (typically
|
|
27
|
+
// pointed at via OLLAMA_API_BASE for the local CLI) are a different product and
|
|
28
|
+
// must not silently redirect cloud requests. Warn once at module load if the
|
|
29
|
+
// env var looks like a non-cloud target so the misconfiguration is visible.
|
|
30
|
+
const CLOUD_BASE_URL = "https://ollama.com";
|
|
31
|
+
const envBase = typeof process !== "undefined" ? process.env?.OLLAMA_API_BASE : undefined;
|
|
32
|
+
if (envBase) {
|
|
33
|
+
// Warn when OLLAMA_API_BASE is set to anything other than the cloud host.
|
|
34
|
+
// Parse the URL so lookalikes (e.g. https://ollama.com.evil.com) are not
|
|
35
|
+
// mistaken for the cloud base. OLLAMA_API_BASE is otherwise ignored: this
|
|
36
|
+
// extension always targets CLOUD_BASE_URL.
|
|
37
|
+
let isCloudBase = false;
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL(envBase);
|
|
40
|
+
isCloudBase = url.protocol === "https:" && url.hostname === "ollama.com";
|
|
41
|
+
} catch {
|
|
42
|
+
// Invalid URL: not the cloud base.
|
|
43
|
+
}
|
|
44
|
+
if (!isCloudBase) {
|
|
45
|
+
console.warn(
|
|
46
|
+
`[pi-ollama-cloud] Ignoring OLLAMA_API_BASE=${envBase}; ` +
|
|
47
|
+
`this extension always targets ${CLOUD_BASE_URL}. ` +
|
|
48
|
+
`Unset OLLAMA_API_BASE (or set it to the cloud URL) to silence this warning.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export const OLLAMA_BASE = CLOUD_BASE_URL.replace(/\/+$/, "");
|
|
14
53
|
|
|
15
54
|
// --- Raw API types ---
|
|
16
55
|
/** Response from POST /api/show */
|
|
@@ -105,7 +144,7 @@ export function assembleModels(raw: Record<string, CachedOllamaModel>): Provider
|
|
|
105
144
|
reasoning: data.capabilities?.includes("thinking") ?? false,
|
|
106
145
|
thinkingLevelMap: resolveThinkingLevelMap(id, data.capabilities ?? []),
|
|
107
146
|
input: (data.capabilities?.includes("vision") ? ["text", "image"] : ["text"]) as ("text" | "image")[],
|
|
108
|
-
cost:
|
|
147
|
+
cost: resolvePrice(id),
|
|
109
148
|
contextWindow: getContextLength(data.model_info ?? {}),
|
|
110
149
|
// No per-model limit exposed by the API (https://docs.ollama.com/api-reference/show-model-details,
|
|
111
150
|
// https://github.com/ollama/ollama/issues/7222). 32768 matches most Ollama Cloud context windows.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ollama-cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"config.ts",
|
|
11
11
|
"models.ts",
|
|
12
12
|
"models.generated.ts",
|
|
13
|
+
"pricing.generated.ts",
|
|
13
14
|
"thinking-levels.ts",
|
|
14
15
|
"utils.ts",
|
|
15
16
|
"web-tools.ts",
|
|
@@ -26,7 +27,8 @@
|
|
|
26
27
|
"lint": "biome check .",
|
|
27
28
|
"format": "biome format --write .",
|
|
28
29
|
"test": "vitest run",
|
|
29
|
-
"
|
|
30
|
+
"smoke:web-tools": "tsx scripts/smoke-web-tools.ts",
|
|
31
|
+
"generate-models": "tsx scripts/generate-pricing.ts && tsx scripts/generate-models.ts && biome format --write models.generated.ts pricing.generated.ts"
|
|
30
32
|
},
|
|
31
33
|
"pi": {
|
|
32
34
|
"extensions": [
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Auto-generated by scripts/generate-pricing.ts
|
|
2
|
+
// Do not edit manually.
|
|
3
|
+
// Generated: 2026-07-17T19:55:25.388Z
|
|
4
|
+
// Model count: 18
|
|
5
|
+
|
|
6
|
+
export interface ModelPrice {
|
|
7
|
+
input: number;
|
|
8
|
+
output: number;
|
|
9
|
+
cacheRead: number;
|
|
10
|
+
cacheWrite: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const MODEL_PRICING: Record<string, ModelPrice> = {
|
|
14
|
+
"deepseek-v4-flash": { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
|
|
15
|
+
"deepseek-v4-pro": { input: 0.435, output: 0.87, cacheRead: 0.003625, cacheWrite: 0 },
|
|
16
|
+
"gemma4:31b": { input: 0.22, output: 0.55, cacheRead: 0.12, cacheWrite: 0 },
|
|
17
|
+
"glm-5.1": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
|
18
|
+
"glm-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
|
19
|
+
"gpt-oss:120b": { input: 0.037, output: 0.17, cacheRead: 0, cacheWrite: 0 },
|
|
20
|
+
"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
|
+
"kimi-k2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
|
|
23
|
+
"kimi-k2.7-code": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
|
|
24
|
+
"minimax-m2.5": { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0.375 },
|
|
25
|
+
"minimax-m2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 },
|
|
26
|
+
"minimax-m3": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
|
|
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 },
|
|
29
|
+
"nemotron-3-super": { input: 0.2, output: 0.8, cacheRead: 0, cacheWrite: 0 },
|
|
30
|
+
"nemotron-3-ultra": { input: 0.5, output: 2.5, cacheRead: 0.15, cacheWrite: 0 },
|
|
31
|
+
"qwen3.5:397b": { input: 0.6, output: 3.6, cacheRead: 0, cacheWrite: 0 },
|
|
32
|
+
};
|
package/thinking-levels.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* - GPT-OSS: no off mode, only low/medium/high
|
|
15
15
|
* - Qwen 3.x (non-VL): binary-only (think/nothink) - off works
|
|
16
16
|
* - Qwen 3 VL: "none" doesn't disable thinking - off is hidden
|
|
17
|
+
* - GLM 5.2: off/high/max are exposed; low/medium are hidden
|
|
17
18
|
* - Kimi K2 Thinking: "none" doesn't disable thinking - off is hidden
|
|
18
19
|
* - MiniMax M2.x: "none" doesn't disable thinking - off is hidden
|
|
19
20
|
*
|
|
@@ -56,6 +57,17 @@ export const QWEN3: ThinkingLevelMap = {
|
|
|
56
57
|
xhigh: null,
|
|
57
58
|
};
|
|
58
59
|
|
|
60
|
+
/** GLM 5.2: Ollama's model page confirms support for "high" and "max" reasoning efforts.
|
|
61
|
+
* https://ollama.com/library/glm-5.2 */
|
|
62
|
+
export const GLM_52: ThinkingLevelMap = {
|
|
63
|
+
off: "none",
|
|
64
|
+
minimal: null,
|
|
65
|
+
low: null,
|
|
66
|
+
medium: null,
|
|
67
|
+
high: "high",
|
|
68
|
+
xhigh: "max",
|
|
69
|
+
};
|
|
70
|
+
|
|
59
71
|
/** "none" doesn't disable thinking - off is hidden.
|
|
60
72
|
* Used by kimi and minimax families. */
|
|
61
73
|
export const NO_OFF: ThinkingLevelMap = {
|
|
@@ -75,6 +87,7 @@ export function resolve(id: string, capabilities: string[]): ThinkingLevelMap |
|
|
|
75
87
|
if (!capabilities.includes("thinking")) return undefined;
|
|
76
88
|
|
|
77
89
|
if (id.startsWith("gpt-oss")) return GPT_OSS;
|
|
90
|
+
if (id === "glm-5.2") return GLM_52;
|
|
78
91
|
if (id.startsWith("qwen3-vl")) return NO_OFF;
|
|
79
92
|
if (id.startsWith("qwen3")) return QWEN3;
|
|
80
93
|
if (id === "kimi-k2-thinking") return NO_OFF;
|
package/utils.ts
CHANGED
|
@@ -2,9 +2,23 @@ export async function fetchJsonWithTimeout<T>(
|
|
|
2
2
|
url: string,
|
|
3
3
|
init: RequestInit,
|
|
4
4
|
timeoutMs: number,
|
|
5
|
+
externalSignal?: AbortSignal,
|
|
5
6
|
): Promise<{ ok: boolean; status: number; data: T | null; error?: string }> {
|
|
6
7
|
const controller = new AbortController();
|
|
7
8
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
9
|
+
|
|
10
|
+
// Link an external abort signal (e.g. a tool's cancellation signal) so the
|
|
11
|
+
// request aborts on either the timeout or the caller aborting. Cleanup runs
|
|
12
|
+
// in the finally block on both the happy and error paths.
|
|
13
|
+
const onExternalAbort = () => controller.abort();
|
|
14
|
+
if (externalSignal) {
|
|
15
|
+
if (externalSignal.aborted) {
|
|
16
|
+
controller.abort();
|
|
17
|
+
} else {
|
|
18
|
+
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
try {
|
|
9
23
|
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
10
24
|
const text = await res.text();
|
|
@@ -25,6 +39,9 @@ export async function fetchJsonWithTimeout<T>(
|
|
|
25
39
|
return { ok: false, status: 0, data: null, error: error instanceof Error ? error.message : String(error) };
|
|
26
40
|
} finally {
|
|
27
41
|
clearTimeout(timeout);
|
|
42
|
+
if (externalSignal && !externalSignal.aborted) {
|
|
43
|
+
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
44
|
+
}
|
|
28
45
|
}
|
|
29
46
|
}
|
|
30
47
|
|
package/web-tools.ts
CHANGED
|
@@ -3,15 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Self-contained module. Depends on:
|
|
5
5
|
* - models.ts - only for OLLAMA_BASE URL constant
|
|
6
|
-
* - pi-coding-agent -
|
|
6
|
+
* - pi-coding-agent - ExtensionAPI, ExtensionContext, keyHint, truncateToVisualLines
|
|
7
7
|
* - pi-tui - Text, truncateToWidth
|
|
8
|
+
* - utils.ts - fetchJsonWithTimeout
|
|
8
9
|
* Does NOT depend on provider registration or model fetching internals.
|
|
10
|
+
*
|
|
11
|
+
* API key resolution: each tool's execute() receives an ExtensionContext whose
|
|
12
|
+
* modelRegistry resolves the registered provider's key (runtime/CLI overrides,
|
|
13
|
+
* the registered apiKey: "$OLLAMA_API_KEY" config, and stored auth.json). The
|
|
14
|
+
* OLLAMA_API_KEY env var is a fallback for when the provider is not yet
|
|
15
|
+
* registered at tool-call time. This avoids direct AuthStorage access, which is
|
|
16
|
+
* not part of the public pi-coding-agent API on 0.80.8+.
|
|
9
17
|
*/
|
|
10
18
|
|
|
11
|
-
import {
|
|
12
|
-
|
|
19
|
+
import {
|
|
20
|
+
type ExtensionAPI,
|
|
21
|
+
type ExtensionContext,
|
|
22
|
+
keyHint,
|
|
23
|
+
type Theme,
|
|
24
|
+
truncateToVisualLines,
|
|
25
|
+
} from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { type Component, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
13
27
|
import { Type } from "@sinclair/typebox";
|
|
14
28
|
import { OLLAMA_BASE } from "./models.ts";
|
|
29
|
+
import { fetchJsonWithTimeout } from "./utils.ts";
|
|
15
30
|
|
|
16
31
|
// --- Types ---
|
|
17
32
|
|
|
@@ -31,10 +46,21 @@ interface FetchResponse {
|
|
|
31
46
|
|
|
32
47
|
// --- Helpers ---
|
|
33
48
|
|
|
34
|
-
const
|
|
49
|
+
const WEB_TOOLS_TIMEOUT_MS = 15000;
|
|
35
50
|
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the Ollama Cloud API key for a tool execution.
|
|
53
|
+
*
|
|
54
|
+
* Prefers the canonical provider auth chain (ctx.modelRegistry.getApiKeyForProvider),
|
|
55
|
+
* which honors runtime/CLI key overrides, the registered
|
|
56
|
+
* apiKey: "$OLLAMA_API_KEY" config, and stored auth.json credentials. Falls back
|
|
57
|
+
* to the OLLAMA_API_KEY env var for the case where the provider is not yet
|
|
58
|
+
* registered at tool-call time.
|
|
59
|
+
*
|
|
60
|
+
* Exported for unit testing.
|
|
61
|
+
*/
|
|
62
|
+
export async function getCloudApiKey(ctx: Pick<ExtensionContext, "modelRegistry">): Promise<string | undefined> {
|
|
63
|
+
return (await ctx.modelRegistry.getApiKeyForProvider("ollama-cloud")) ?? process.env.OLLAMA_API_KEY;
|
|
38
64
|
}
|
|
39
65
|
|
|
40
66
|
function noApiKeyError() {
|
|
@@ -49,6 +75,58 @@ function noApiKeyError() {
|
|
|
49
75
|
};
|
|
50
76
|
}
|
|
51
77
|
|
|
78
|
+
/** Search error response for a non-ok result, mapping distinct status codes. */
|
|
79
|
+
function searchError(status: number, error?: string) {
|
|
80
|
+
if (status === 401 || status === 403) {
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
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
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (status === 429) {
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text" as const, text: "Ollama Cloud search failed: rate limited. Try again shortly." }],
|
|
95
|
+
isError: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: "text" as const, text: `Search API error (status ${status}): ${error || "unknown error"}` }],
|
|
100
|
+
isError: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Fetch error response for a non-ok result, mapping distinct status codes. */
|
|
105
|
+
function fetchError(status: number, error?: string) {
|
|
106
|
+
if (status === 401 || status === 403) {
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
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
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (status === 429) {
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: "text" as const, text: "Ollama Cloud fetch failed: rate limited. Try again shortly." }],
|
|
121
|
+
isError: true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: "text" as const, text: `Fetch API error (status ${status}): ${error || "unknown error"}` }],
|
|
126
|
+
isError: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
52
130
|
const PREVIEW_LINES = 8;
|
|
53
131
|
|
|
54
132
|
/**
|
|
@@ -59,10 +137,10 @@ function createRenderResult() {
|
|
|
59
137
|
return (
|
|
60
138
|
result: { content: Array<{ type: string; text: string }>; isError?: boolean },
|
|
61
139
|
options: { expanded: boolean; isPartial: boolean },
|
|
62
|
-
theme:
|
|
140
|
+
theme: Theme,
|
|
63
141
|
context: {
|
|
64
142
|
invalidate: () => void;
|
|
65
|
-
lastComponent:
|
|
143
|
+
lastComponent: Component | undefined;
|
|
66
144
|
state: { cachedWidth?: number; cachedLines?: string[]; cachedSkipped?: number };
|
|
67
145
|
},
|
|
68
146
|
) => {
|
|
@@ -107,6 +185,18 @@ function createRenderResult() {
|
|
|
107
185
|
};
|
|
108
186
|
}
|
|
109
187
|
|
|
188
|
+
/** Validate a parsed web_search response: must have a results array. */
|
|
189
|
+
function isSearchResponse(data: unknown): data is SearchResponse {
|
|
190
|
+
return data != null && typeof data === "object" && Array.isArray((data as SearchResponse).results);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Validate a parsed web_fetch response: must have string title/content and a links array. */
|
|
194
|
+
function isFetchResponse(data: unknown): data is FetchResponse {
|
|
195
|
+
if (data == null || typeof data !== "object") return false;
|
|
196
|
+
const d = data as FetchResponse;
|
|
197
|
+
return typeof d.title === "string" && typeof d.content === "string" && Array.isArray(d.links);
|
|
198
|
+
}
|
|
199
|
+
|
|
110
200
|
// --- Registrations ---
|
|
111
201
|
|
|
112
202
|
export function registerWebSearchTool(pi: ExtensionAPI) {
|
|
@@ -128,12 +218,13 @@ export function registerWebSearchTool(pi: ExtensionAPI) {
|
|
|
128
218
|
}),
|
|
129
219
|
),
|
|
130
220
|
}),
|
|
131
|
-
async execute(_toolCallId, params, signal, _onUpdate,
|
|
132
|
-
const apiKey = await getCloudApiKey();
|
|
221
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
222
|
+
const apiKey = await getCloudApiKey(ctx);
|
|
133
223
|
if (!apiKey) return noApiKeyError();
|
|
134
224
|
|
|
135
|
-
|
|
136
|
-
|
|
225
|
+
const res = await fetchJsonWithTimeout<SearchResponse>(
|
|
226
|
+
`${OLLAMA_BASE}/api/web_search`,
|
|
227
|
+
{
|
|
137
228
|
method: "POST",
|
|
138
229
|
headers: {
|
|
139
230
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -143,53 +234,27 @@ export function registerWebSearchTool(pi: ExtensionAPI) {
|
|
|
143
234
|
query: params.query,
|
|
144
235
|
max_results: params.max_results ?? 5,
|
|
145
236
|
}),
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const errorText = await res.text().catch(() => "");
|
|
151
|
-
if (res.status === 401 || res.status === 403) {
|
|
152
|
-
return {
|
|
153
|
-
content: [
|
|
154
|
-
{
|
|
155
|
-
type: "text",
|
|
156
|
-
text:
|
|
157
|
-
"Ollama Cloud search failed: authentication error. " +
|
|
158
|
-
"Check your API key in OLLAMA_API_KEY or auth.json.",
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
isError: true,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
if (res.status === 429) {
|
|
165
|
-
return {
|
|
166
|
-
content: [{ type: "text", text: "Ollama Cloud search failed: rate limited. Try again shortly." }],
|
|
167
|
-
isError: true,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
return {
|
|
171
|
-
content: [
|
|
172
|
-
{ type: "text", text: `Search API error (status ${res.status}): ${errorText || res.statusText}` },
|
|
173
|
-
],
|
|
174
|
-
isError: true,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
237
|
+
},
|
|
238
|
+
WEB_TOOLS_TIMEOUT_MS,
|
|
239
|
+
signal,
|
|
240
|
+
);
|
|
177
241
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
.map((r, i) => `${i + 1}. ${r.title}\n URL: ${r.url}\n ${r.content}`)
|
|
181
|
-
.join("\n\n");
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
content: [{ type: "text", text: formatted || "No results found." }],
|
|
185
|
-
details: { results: data.results },
|
|
186
|
-
};
|
|
187
|
-
} catch (err) {
|
|
242
|
+
if (!res.ok) return searchError(res.status, res.error);
|
|
243
|
+
if (!isSearchResponse(res.data)) {
|
|
188
244
|
return {
|
|
189
|
-
content: [{ type: "text", text:
|
|
245
|
+
content: [{ type: "text", text: "Web search failed: unexpected response shape from the API." }],
|
|
190
246
|
isError: true,
|
|
191
247
|
};
|
|
192
248
|
}
|
|
249
|
+
|
|
250
|
+
const formatted = res.data.results
|
|
251
|
+
.map((r, i) => `${i + 1}. ${r.title}\n URL: ${r.url}\n ${r.content}`)
|
|
252
|
+
.join("\n\n");
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
content: [{ type: "text", text: formatted || "No results found." }],
|
|
256
|
+
details: { results: res.data.results },
|
|
257
|
+
};
|
|
193
258
|
},
|
|
194
259
|
renderCall(args, theme, _context) {
|
|
195
260
|
const display = args.query ? `ollama_web_search("${args.query}")` : "ollama_web_search";
|
|
@@ -210,69 +275,47 @@ export function registerWebFetchTool(pi: ExtensionAPI) {
|
|
|
210
275
|
parameters: Type.Object({
|
|
211
276
|
url: Type.String({ description: "URL to fetch and extract content from", format: "uri" }),
|
|
212
277
|
}),
|
|
213
|
-
async execute(_toolCallId, params, signal, _onUpdate,
|
|
214
|
-
const apiKey = await getCloudApiKey();
|
|
278
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
279
|
+
const apiKey = await getCloudApiKey(ctx);
|
|
215
280
|
if (!apiKey) return noApiKeyError();
|
|
216
281
|
|
|
217
|
-
|
|
218
|
-
|
|
282
|
+
const res = await fetchJsonWithTimeout<FetchResponse>(
|
|
283
|
+
`${OLLAMA_BASE}/api/web_fetch`,
|
|
284
|
+
{
|
|
219
285
|
method: "POST",
|
|
220
286
|
headers: {
|
|
221
287
|
Authorization: `Bearer ${apiKey}`,
|
|
222
288
|
"Content-Type": "application/json",
|
|
223
289
|
},
|
|
224
290
|
body: JSON.stringify({ url: params.url }),
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const errorText = await res.text().catch(() => "");
|
|
230
|
-
if (res.status === 401 || res.status === 403) {
|
|
231
|
-
return {
|
|
232
|
-
content: [
|
|
233
|
-
{
|
|
234
|
-
type: "text",
|
|
235
|
-
text:
|
|
236
|
-
"Ollama Cloud fetch failed: authentication error. " +
|
|
237
|
-
"Check your API key in OLLAMA_API_KEY or auth.json.",
|
|
238
|
-
},
|
|
239
|
-
],
|
|
240
|
-
isError: true,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
if (res.status === 429) {
|
|
244
|
-
return {
|
|
245
|
-
content: [{ type: "text", text: "Ollama Cloud fetch failed: rate limited. Try again shortly." }],
|
|
246
|
-
isError: true,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
return {
|
|
250
|
-
content: [{ type: "text", text: `Fetch API error (status ${res.status}): ${errorText || res.statusText}` }],
|
|
251
|
-
isError: true,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
291
|
+
},
|
|
292
|
+
WEB_TOOLS_TIMEOUT_MS,
|
|
293
|
+
signal,
|
|
294
|
+
);
|
|
254
295
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
`Title: ${data.title}`,
|
|
258
|
-
"",
|
|
259
|
-
"Content:",
|
|
260
|
-
data.content,
|
|
261
|
-
"",
|
|
262
|
-
`Links found: ${data.links?.length ?? 0}`,
|
|
263
|
-
...(data.links?.slice(0, 10).map((l) => ` - ${l}`) ?? []),
|
|
264
|
-
].join("\n");
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
content: [{ type: "text", text: formatted }],
|
|
268
|
-
details: { title: data.title, content: data.content, links: data.links },
|
|
269
|
-
};
|
|
270
|
-
} catch (err) {
|
|
296
|
+
if (!res.ok) return fetchError(res.status, res.error);
|
|
297
|
+
if (!isFetchResponse(res.data)) {
|
|
271
298
|
return {
|
|
272
|
-
content: [{ type: "text", text:
|
|
299
|
+
content: [{ type: "text", text: "Web fetch failed: unexpected response shape from the API." }],
|
|
273
300
|
isError: true,
|
|
274
301
|
};
|
|
275
302
|
}
|
|
303
|
+
|
|
304
|
+
const data = res.data;
|
|
305
|
+
const formatted = [
|
|
306
|
+
`Title: ${data.title}`,
|
|
307
|
+
"",
|
|
308
|
+
"Content:",
|
|
309
|
+
data.content,
|
|
310
|
+
"",
|
|
311
|
+
`Links found: ${data.links?.length ?? 0}`,
|
|
312
|
+
...(data.links?.slice(0, 10).map((l) => ` - ${l}`) ?? []),
|
|
313
|
+
].join("\n");
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
content: [{ type: "text", text: formatted }],
|
|
317
|
+
details: { title: data.title, content: data.content, links: data.links },
|
|
318
|
+
};
|
|
276
319
|
},
|
|
277
320
|
renderCall(args, theme, _context) {
|
|
278
321
|
const display = args.url ? `ollama_web_fetch("${args.url}")` : "ollama_web_fetch";
|