pi-ollama-cloud 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,21 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [Unreleased]
5
+ ## [0.12.0] - 2026-09-11
6
+
7
+ - Source per-model thinking levels from models.dev instead of hardcoded maps. `scripts/generate-reasoning.ts` fetches the `ollama-cloud` provider's `reasoning_options` into `reasoning.generated.ts`, and `thinking-levels.ts` maps each model's effort values onto Pi's levels (toggle-only models become a binary on/off map; models with no models.dev entry fall back to `DEFAULT`). The `off` switch is handled by a small override table for models verified not to honor `reasoning_effort:"none"` (`gpt-oss:20b`, `gpt-oss:120b`, `minimax-m2.7`). Removed the now-stale per-family maps and the `docs/think-experiment.md` doc.
8
+ - `generate-models` now also refreshes `reasoning.generated.ts` (runs `generate-pricing`, `generate-reasoning`, then `generate-models`).
9
+ - Fix `generate-pricing` mis-dropping models whose pricing-page cached-input cell is `-` (no cache rate): those rows now match and their `cacheRead` equals `input`. This restored pricing for `mistral-large-3:675b`, `nemotron-3-nano:30b`, and `qwen3.5:397b`, which the earlier regex had left at zero cost.
10
+ - Refresh the model catalog: added `deepseek-v4.1-flash` (probed max output 393216).
11
+
12
+ ## [0.11.0] - 2026-09-07
13
+
14
+ - Fix `/ollama-cloud-usage` and the usage status bar failing with "unexpected response shape" after the undocumented `/api/usage` endpoint flipped between a single `limits.monthly` bucket and `limits.session` plus `limits.weekly` (the shape has flip-flopped repeatedly as of 2026-09). Any bucket present (`monthly`, `session`, `weekly`) is accepted alone or in combination, and whichever are present are displayed as `5h`/`7d`/`30d` segments. Thanks @johanngyger (#56).
15
+ - Cache `ollama_web_search` results (24h) and `ollama_web_fetch` pages (24h success / 15 min failure) on disk under the pi agent home, so repeated queries and page reads cost 0 API calls. Expired entries are pruned on write, the cache is capped at 500 entries per kind (oldest evicted beyond the cap; `PI_OLLAMA_SEARCH_MAX_ENTRIES`), a partially corrupted cache file is validated per entry and degrades to "no cache" instead of crashing tool calls, and the file is written with `0600` permissions since it stores page content and URLs that can embed credentials. Tune with `PI_OLLAMA_SEARCH_TTL_HOURS`, `PI_OLLAMA_SEARCH_FAIL_TTL_MINUTES`, `PI_OLLAMA_SEARCH_MAX_ENTRIES`, and `PI_OLLAMA_SEARCH_CACHE_PATH`.
16
+ - Bound web tool context usage: search snippets truncate to 500 chars with `[truncated]`/`[complete]` markers, `expand=<index>` returns a truncated result's full content from the cached search (0 extra API calls), and `ollama_web_fetch` pages long pages in 3000-char chunks via `offset`/`full` with a `Continue:` hint for the next offset (`PI_OLLAMA_SEARCH_SNIPPET_CHARS`/`PI_OLLAMA_SEARCH_CHUNK_CHARS` to tune).
17
+ - Add `refresh=true` to both web tools to bypass the cache (including a cached failure) and re-call the API; the fresh result replaces the cache entry.
18
+ - Failed page fetches are negative-cached for 15 min with a diagnostic message (likely cause + next steps) instead of a bare error. Auth (401/403), rate-limit (429), transport (timeout/abort/network), server (5xx), and unexpected-response-shape failures are never cached — search reports transport errors as `transport error` instead of a confusing `status 0` — so retrying after a fixed key, an expired rate-limit window, or a transient server blip re-calls the API immediately.
19
+ - Note: `ollama_web_fetch` tool results now carry `details: { title, totalChars, links }` (previously `{ title, content, links }`); paged content is read via the tool output text, not `details.content`.
6
20
 
7
21
  ## [0.10.0] - 2026-09-03
8
22
 
package/README.md CHANGED
@@ -7,7 +7,7 @@ Registers Ollama Cloud as a model provider with dynamically fetched models, and
7
7
  ## Features
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
- - **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.
10
+ - **Data-driven thinking levels** - Maps Pi's thinking levels to Ollama Cloud's OpenAI-compatible `reasoning_effort` values via `thinking-levels.ts`, sourced from models.dev per-model reasoning options with a small override table for the models where `none` doesn't disable thinking.
11
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
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.
13
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.
@@ -132,7 +132,7 @@ Model metadata is derived from the `/api/show` response:
132
132
  | Field | Source |
133
133
  |---|---|
134
134
  | `reasoning` | `capabilities` includes `"thinking"` |
135
- | `thinkingLevelMap` | [`thinking-levels.ts`](thinking-levels.ts) with 5 maps (DEFAULT, GPT_OSS, QWEN3, GLM_52, NO_OFF) based on API testing |
135
+ | `thinkingLevelMap` | [`thinking-levels.ts`](thinking-levels.ts) + [`reasoning.generated.ts`](reasoning.generated.ts) (models.dev reasoning options), with an `off` override table for models that ignore `none` |
136
136
  | `input` | `["text", "image"]` if `capabilities` includes `"vision"`, else `["text"]` |
137
137
  | `contextWindow` | `model_info.*.context_length` (falls back to 128000) |
138
138
  | `maxTokens` | Probed per-model limits from [`limits.generated.ts`](limits.generated.ts), generated by `scripts/generate-limits.ts` (requires `OLLAMA_API_KEY`). Models without a probed limit fall back to 32768. |
@@ -146,17 +146,11 @@ Cache pricing is informational only: the `/pricing` page lists a "Cached input"
146
146
 
147
147
  ### Thinking level mapping
148
148
 
149
- Pi's thinking levels are mapped to Ollama Cloud's OpenAI-compatible `reasoning_effort` parameter in [`thinking-levels.ts`](thinking-levels.ts). The API accepts `none`, `low`, `medium`, `high`, and `max`. Effects of `max` over `high` vary by model and prompt difficulty - see [`docs/think-experiment.md`](docs/think-experiment.md) for details.
149
+ Pi's thinking levels are mapped to Ollama Cloud's OpenAI-compatible `reasoning_effort` parameter in [`thinking-levels.ts`](thinking-levels.ts). The API accepts `none`, `low`, `medium`, `high`, `xhigh`, and `max`. Effects of `max` over `high` vary by model and prompt difficulty.
150
150
 
151
- | Map | Models | Levels exposed | Notes |
152
- |---|---|---|---|
153
- | `DEFAULT` | Most thinking models | off, low, medium, high, xhigh | `minimal` hidden (duplicate of low) |
154
- | `GPT_OSS` | `gpt-oss*` | low, medium, high | Can't disable thinking, no off or xhigh |
155
- | `QWEN3` | `qwen3*` (except `qwen3-vl*`) | off, medium | Binary-only (think/nothink), no gradation |
156
- | `GLM_52` | `glm-5.2` | off, high, xhigh | GLM supports disabled thinking; Ollama's model page confirms `high` and `max` reasoning efforts |
157
- | `NO_OFF` | `qwen3-vl*`, `kimi-k2-thinking`, `minimax*` | low, medium, high, xhigh | "none" doesn't disable thinking on these models |
151
+ Per-model support is sourced from models.dev: [`scripts/generate-reasoning.ts`](scripts/generate-reasoning.ts) fetches the `ollama-cloud` provider's `reasoning_options` into `reasoning.generated.ts`, and `resolve()` maps each model's effort values onto Pi's levels. Models with `effort` values expose those grades; `toggle`-only models expose a single on/off level. Models with no models.dev entry fall back to `DEFAULT`.
158
152
 
159
- See [docs/think-experiment.md](docs/think-experiment.md) for the testing methodology and results.
153
+ Because the API reports only a boolean `thinking` capability and models.dev does not reliably encode the `none` behavior, the `off` switch is handled via a small override table in `thinking-levels.ts`: it defaults to enabled, and is hidden only for models verified (by live probing) not to honor `reasoning_effort:"none"` - currently `gpt-oss:20b`, `gpt-oss:120b`, and `minimax-m2.7`. The per-model metadata gaps behind the models.dev sourcing are tracked upstream in [ollama/ollama#18385](https://github.com/ollama/ollama/issues/18385).
160
154
 
161
155
  ## Tools
162
156
 
@@ -167,19 +161,60 @@ See [docs/think-experiment.md](docs/think-experiment.md) for the testing methodo
167
161
 
168
162
  Both tools use the same Ollama Cloud API key configured for the provider. No local Ollama server is needed.
169
163
 
164
+ ### Caching
165
+
166
+ Both tools cache results on disk (under the pi agent home, `~/.pi/agent/cache/pi-ollama-cloud/cache.json`). A repeated search query or page fetch within the TTL is served from cache and costs 0 API calls:
167
+
168
+ - Successful searches and pages: cached for 24h
169
+ - Failed page fetches: negative-cached for 15 min, so retrying a dead page does not re-call the API. Auth (401/403), rate-limit (429), transport (timeouts, aborts, network errors), and server (5xx) failures are not cached — fixing the key, waiting out the limit, or a transient blip lets a retry through immediately
170
+ - Expired entries are pruned on write and the cache is capped at 500 entries per kind (searches/pages), evicting the oldest first. This bounds entry count, not file size: full page and search content can still make `cache.json` large, and loading it parses the whole file
171
+ - The cache file is written with `0600` permissions. It stores full page content and raw URLs, which can embed credentials in query strings — avoid fetching URLs that carry secrets in the query string, or set a custom `PI_OLLAMA_SEARCH_CACHE_PATH`
172
+ - Concurrent pi processes share the cache file on a last-writer-wins basis (no cross-process locking): one process's save can drop another's fresh entries, at the cost of a redundant API call
173
+ - `refresh=true` on either tool bypasses the cache (including a cached failure) and re-calls the API; the fresh result replaces the cache entry
174
+
175
+ ### `ollama_web_search`
176
+
177
+ Returns up to 5 results by default (`max_results`, max 10; title, URL, 500-char snippet). Snippets are marked `[truncated]` when the source is longer than the snippet. Output ends with `# live query` or `# from cache` to show whether the API was called.
178
+
179
+ The search API returns each result's full content; it is cached in full, so a truncated result can be expanded without a separate fetch:
180
+
181
+ - `expand=<index>` — return the full content of that result (1-based) from the cached search, 0 extra API calls. The cache key includes `max_results`, so expanding hits the cache only when the query was searched with the same `max_results`; otherwise the search runs live first.
182
+ - Use `ollama_web_fetch` only when the search result's content is not enough (e.g. you need a different page, or the search excerpt is shorter than the full page).
183
+
184
+ ### `ollama_web_fetch`
185
+
186
+ Returns the page title, a 3000-char slice of the content, and links. Long pages are read in chunks to keep the context window small:
187
+
188
+ - `offset=N` — continue reading from character N (the output tells you the next offset)
189
+ - `full=true` — return all remaining content from `offset` in one call
190
+
191
+ A failed fetch throws a diagnostic message (likely cause + next steps) instead of a bare error.
192
+
193
+ ### Tuning
194
+
195
+ | Env var | Default | Meaning |
196
+ |---|---|---|
197
+ | `PI_OLLAMA_SEARCH_TTL_HOURS` | `24` | Success cache TTL |
198
+ | `PI_OLLAMA_SEARCH_FAIL_TTL_MINUTES` | `15` | Failure (negative) cache TTL |
199
+ | `PI_OLLAMA_SEARCH_CACHE_PATH` | `<pi agent home>/cache/pi-ollama-cloud/cache.json` | Cache file location |
200
+ | `PI_OLLAMA_SEARCH_MAX_ENTRIES` | `500` | Max cached entries per kind (searches/pages); oldest evicted beyond the cap |
201
+ | `PI_OLLAMA_SEARCH_SNIPPET_CHARS` | `500` | Search snippet length |
202
+ | `PI_OLLAMA_SEARCH_CHUNK_CHARS` | `3000` | Fetch chunk size |
203
+
170
204
  ## Commands
171
205
 
172
206
  | Command | Description |
173
207
  |---|---|
174
208
  | `/ollama-webtools [on\|off\|enable\|disable]` | Enable or disable the `ollama_web_search` and `ollama_web_fetch` tools. Toggles if no argument given. |
175
- | `/ollama-cloud-usage` | Show Ollama Cloud monthly usage limits, per-model request counts, and the 4-week activity cost. |
209
+ | `/ollama-cloud-usage` | Show Ollama Cloud usage limits (one section per limit bucket the API reports), per-model request counts, and the 4-week activity cost. |
176
210
  | `/ollama-usage-status [on\|off\|enable\|disable]` | Enable or disable the footer usage status bar. Toggles if no argument given. |
177
211
 
178
212
  ## Usage status bar
179
213
 
180
214
  While an `ollama-cloud` model is the active provider, the footer shows a compact
181
- live usage readout (`30d ▕███░░░░░░░▏ 34%`) that refreshes
182
- every 5 minutes and after each agent turn (but no more often than every 5 minutes). It is colored by how close
215
+ live usage readout with one segment per limit bucket the API reports
216
+ (`5h ▕███░░░░░░░▏ 34% 7d ▕█░░░░░░░░░▏ 7%`, or a single `30d` segment) that
217
+ refreshes every 5 minutes and after each agent turn (but no more often than every 5 minutes). It is colored by how close
183
218
  it is to the cap: green below 60%, yellow at 60-79%, red at 80%+. It reads the
184
219
  same undocumented `/api/usage` endpoint as `/ollama-cloud-usage` and clears
185
220
  itself on transient errors or when you switch to a non-Ollama-Cloud provider.
package/cache.ts ADDED
@@ -0,0 +1,181 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
+ import { envInt } from "./utils.ts";
6
+
7
+ export const CACHE_PATH =
8
+ process.env.PI_OLLAMA_SEARCH_CACHE_PATH ?? join(getAgentDir(), "cache", "pi-ollama-cloud", "cache.json");
9
+ export const CACHE_TTL_MS = envInt("PI_OLLAMA_SEARCH_TTL_HOURS", 24) * 60 * 60 * 1000;
10
+ export const FAIL_TTL_MS = envInt("PI_OLLAMA_SEARCH_FAIL_TTL_MINUTES", 15) * 60 * 1000;
11
+ /** Max entries per map (searches/pages); oldest-ts entries are evicted beyond this. */
12
+ export const MAX_ENTRIES = envInt("PI_OLLAMA_SEARCH_MAX_ENTRIES", 500);
13
+
14
+ export interface SearchResult {
15
+ title: string;
16
+ url: string;
17
+ content: string;
18
+ }
19
+
20
+ export interface SearchCacheEntry {
21
+ ts: number;
22
+ q: string;
23
+ results: SearchResult[];
24
+ }
25
+
26
+ export interface PageCacheEntry {
27
+ ts: number;
28
+ status?: number;
29
+ title?: string;
30
+ content?: string;
31
+ links?: string[] | null;
32
+ error?: string;
33
+ errorType?: "response-shape";
34
+ }
35
+
36
+ export interface CacheData {
37
+ searches: Record<string, SearchCacheEntry>;
38
+ pages: Record<string, PageCacheEntry>;
39
+ }
40
+
41
+ interface CacheOptions {
42
+ path: string;
43
+ ttlMs: number;
44
+ failTtlMs: number;
45
+ maxEntries: number;
46
+ }
47
+
48
+ function isRecord(v: unknown): v is Record<string, unknown> {
49
+ return typeof v === "object" && v !== null && !Array.isArray(v);
50
+ }
51
+
52
+ /** Keys that must never come from a parsed JSON file (prototype pollution). */
53
+ const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
54
+
55
+ export function isSafeKey(key: string): boolean {
56
+ return !UNSAFE_KEYS.has(key);
57
+ }
58
+
59
+ /** Shallow shape checks so a partially corrupted cache file degrades instead of crashing tool calls. */
60
+ function isSearchEntry(v: unknown): v is SearchCacheEntry {
61
+ return (
62
+ isRecord(v) &&
63
+ typeof v.ts === "number" &&
64
+ typeof v.q === "string" &&
65
+ Array.isArray(v.results) &&
66
+ v.results.every(
67
+ (r) => isRecord(r) && typeof r.title === "string" && typeof r.url === "string" && typeof r.content === "string",
68
+ )
69
+ );
70
+ }
71
+
72
+ function isPageEntry(v: unknown): v is PageCacheEntry {
73
+ if (!isRecord(v) || typeof v.ts !== "number") return false;
74
+ const fieldsValid =
75
+ (v.status === undefined || typeof v.status === "number") &&
76
+ (v.title === undefined || typeof v.title === "string") &&
77
+ (v.content === undefined || typeof v.content === "string") &&
78
+ (v.links === null ||
79
+ v.links === undefined ||
80
+ (Array.isArray(v.links) && v.links.every((l) => typeof l === "string"))) &&
81
+ (v.error === undefined || (typeof v.error === "string" && v.error !== "")) &&
82
+ (v.errorType === undefined || v.errorType === "response-shape");
83
+ if (!fieldsValid) return false;
84
+ // Must be either a real failure or a real success; anything else (e.g. an
85
+ // entry with neither content nor a non-empty error) would render as a fake
86
+ // empty success.
87
+ return (
88
+ (typeof v.error === "string" && v.error !== "") || (typeof v.title === "string" && typeof v.content === "string")
89
+ );
90
+ }
91
+
92
+ export interface CacheStore {
93
+ loadCache(): CacheData;
94
+ saveCache(): void;
95
+ isFresh(entry: { ts: number; error?: string } | undefined): boolean;
96
+ }
97
+
98
+ export function createCache(options: Partial<CacheOptions> = {}): CacheStore {
99
+ const path = options.path ?? CACHE_PATH;
100
+ const ttlMs = options.ttlMs ?? CACHE_TTL_MS;
101
+ const failTtlMs = options.failTtlMs ?? FAIL_TTL_MS;
102
+ const maxEntries = options.maxEntries ?? MAX_ENTRIES;
103
+ let cacheData: CacheData | null = null;
104
+
105
+ function loadCache(): CacheData {
106
+ if (cacheData) return cacheData;
107
+ try {
108
+ const raw: unknown = JSON.parse(readFileSync(path, "utf8"));
109
+ if (isRecord(raw) && isRecord(raw.searches) && isRecord(raw.pages)) {
110
+ // Per-entry validation: drop poisoned entries so a partially corrupt
111
+ // file degrades to "those entries are gone" instead of crashing calls.
112
+ cacheData = { searches: {}, pages: {} };
113
+ for (const [key, entry] of Object.entries(raw.searches)) {
114
+ if (isSafeKey(key) && isSearchEntry(entry)) cacheData.searches[key] = entry;
115
+ }
116
+ for (const [key, entry] of Object.entries(raw.pages)) {
117
+ if (isSafeKey(key) && isPageEntry(entry)) cacheData.pages[key] = entry;
118
+ }
119
+ return cacheData;
120
+ }
121
+ } catch {
122
+ // First run or corrupt file, start fresh.
123
+ }
124
+ cacheData = { searches: {}, pages: {} };
125
+ return cacheData;
126
+ }
127
+
128
+ function isFresh(entry: { ts: number; error?: string } | undefined): boolean {
129
+ if (!entry) return false;
130
+ // A future ts (hand-edited file) would otherwise be fresh forever; treat as stale.
131
+ if (entry.ts > Date.now()) return false;
132
+ return Date.now() - entry.ts < (entry.error ? failTtlMs : ttlMs);
133
+ }
134
+
135
+ function evictOldest(map: Record<string, { ts: number }>): void {
136
+ const keys = Object.keys(map);
137
+ if (keys.length <= maxEntries) return;
138
+ const overflow = keys.sort((a, b) => map[a].ts - map[b].ts).slice(0, keys.length - maxEntries);
139
+ for (const key of overflow) delete map[key];
140
+ }
141
+
142
+ function saveCache(): void {
143
+ const data = loadCache();
144
+ for (const [key, entry] of Object.entries(data.searches)) if (!isFresh(entry)) delete data.searches[key];
145
+ for (const [key, entry] of Object.entries(data.pages)) if (!isFresh(entry)) delete data.pages[key];
146
+ // TTL bounds entry age; the cap bounds entry count so an aggressive session
147
+ // cannot grow the file without limit.
148
+ evictOldest(data.searches);
149
+ evictOldest(data.pages);
150
+ try {
151
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
152
+ // Use a unique temporary path so concurrent processes cannot overwrite
153
+ // one another's in-progress writes.
154
+ const tmp = `${path}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
155
+ try {
156
+ // 0o600: the cache stores full page content and URLs, which can embed
157
+ // credentials in query strings; it should not be world-readable.
158
+ writeFileSync(tmp, JSON.stringify(data), { mode: 0o600 });
159
+ renameSync(tmp, path);
160
+ // Fix perms of a file written by a pre-0600 version.
161
+ chmodSync(path, 0o600);
162
+ } catch (error) {
163
+ rmSync(tmp, { force: true });
164
+ throw error;
165
+ }
166
+ } catch {
167
+ // Cache is best-effort; a failed write must not break the tool call.
168
+ }
169
+ }
170
+
171
+ return { loadCache, saveCache, isFresh };
172
+ }
173
+
174
+ export const defaultCache = createCache();
175
+ export const loadCache = defaultCache.loadCache;
176
+ export const saveCache = defaultCache.saveCache;
177
+ export const isFresh = defaultCache.isFresh;
178
+
179
+ export function searchCacheKey(query: string, maxResults: number): string {
180
+ return createHash("sha1").update(`${query}\n${maxResults}`).digest("hex");
181
+ }
package/index.ts CHANGED
@@ -134,7 +134,7 @@ export default async function (pi: ExtensionAPI) {
134
134
  // --- Usage Command ---
135
135
 
136
136
  pi.registerCommand("ollama-cloud-usage", {
137
- description: "Show Ollama Cloud monthly usage limits.",
137
+ description: "Show Ollama Cloud usage limits.",
138
138
  handler: async (_args, ctx) => {
139
139
  const apiKey = await getCloudApiKey(ctx);
140
140
  if (!apiKey) {
@@ -152,7 +152,7 @@ export default async function (pi: ExtensionAPI) {
152
152
 
153
153
  // --- Usage Status Bar ---
154
154
 
155
- // Footer status showing live monthly usage while ollama-cloud is the
155
+ // Footer status showing live usage while ollama-cloud is the
156
156
  // active provider. Refreshes on a 5-minute timer; agent_end also triggers a
157
157
  // refresh but is throttled to the same cooldown so a turn never hammers the
158
158
  // undocumented /api/usage endpoint. The quota-bar concept is inspired by
@@ -1,10 +1,11 @@
1
1
  // Auto-generated by scripts/generate-limits.ts
2
2
  // Do not edit manually.
3
- // Probed models: 19 (0 failed)
3
+ // Entries: 20
4
4
 
5
5
  export const MODEL_MAX_OUTPUT_TOKENS: Record<string, number> = {
6
6
  "deepseek-v4-flash:0731": 65536,
7
7
  "deepseek-v4-pro:0813": 65536,
8
+ "deepseek-v4.1-flash": 393216,
8
9
  "gemma4:31b": 262144,
9
10
  "glm-5.1": 131072,
10
11
  "glm-5.2": 131072,
@@ -1,7 +1,7 @@
1
1
  // Auto-generated by scripts/generate-models.ts
2
2
  // Do not edit manually.
3
- // Generated: 2026-09-03T10:12:02.243Z
4
- // Model count: 19
3
+ // Generated: 2026-09-12T02:41:57.025Z
4
+ // Model count: 20
5
5
 
6
6
  import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
7
7
 
@@ -39,8 +39,8 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
39
39
  reasoning: true,
40
40
  thinkingLevelMap: {
41
41
  high: "high",
42
- low: "low",
43
- medium: "medium",
42
+ low: null,
43
+ medium: null,
44
44
  minimal: null,
45
45
  off: "none",
46
46
  xhigh: "max",
@@ -77,10 +77,50 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
77
77
  input: ["text"],
78
78
  maxTokens: 65536,
79
79
  reasoning: true,
80
+ thinkingLevelMap: {
81
+ high: "high",
82
+ low: null,
83
+ medium: null,
84
+ minimal: null,
85
+ off: "none",
86
+ xhigh: "max",
87
+ },
88
+ },
89
+ {
90
+ id: "deepseek-v4.1-flash",
91
+ name: "deepseek-v4.1-flash",
92
+ compat: {
93
+ maxTokensField: "max_tokens",
94
+ openRouterRouting: {},
95
+ requiresAssistantAfterToolResult: false,
96
+ requiresReasoningContentOnAssistantMessages: false,
97
+ requiresThinkingAsText: false,
98
+ requiresToolResultName: false,
99
+ sendSessionAffinityHeaders: false,
100
+ supportsDeveloperRole: false,
101
+ supportsLongCacheRetention: false,
102
+ supportsReasoningEffort: true,
103
+ supportsStore: false,
104
+ supportsStrictMode: false,
105
+ supportsUsageInStreaming: true,
106
+ thinkingFormat: "openai",
107
+ vercelGatewayRouting: {},
108
+ zaiToolStream: false,
109
+ },
110
+ contextWindow: 1048576,
111
+ cost: {
112
+ cacheRead: 0.006,
113
+ cacheWrite: 0,
114
+ input: 0.3,
115
+ output: 1.2,
116
+ },
117
+ input: ["text", "image"],
118
+ maxTokens: 393216,
119
+ reasoning: true,
80
120
  thinkingLevelMap: {
81
121
  high: "high",
82
122
  low: "low",
83
- medium: "medium",
123
+ medium: null,
84
124
  minimal: null,
85
125
  off: "none",
86
126
  xhigh: "max",
@@ -118,12 +158,12 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
118
158
  maxTokens: 262144,
119
159
  reasoning: true,
120
160
  thinkingLevelMap: {
121
- high: "high",
122
- low: "low",
161
+ high: null,
162
+ low: null,
123
163
  medium: "medium",
124
164
  minimal: null,
125
165
  off: "none",
126
- xhigh: "max",
166
+ xhigh: null,
127
167
  },
128
168
  },
129
169
  {
@@ -158,12 +198,12 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
158
198
  maxTokens: 131072,
159
199
  reasoning: true,
160
200
  thinkingLevelMap: {
161
- high: "high",
162
- low: "low",
201
+ high: null,
202
+ low: null,
163
203
  medium: "medium",
164
204
  minimal: null,
165
205
  off: "none",
166
- xhigh: "max",
206
+ xhigh: null,
167
207
  },
168
208
  },
169
209
  {
@@ -240,7 +280,7 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
240
280
  thinkingLevelMap: {
241
281
  high: "high",
242
282
  low: "low",
243
- medium: "medium",
283
+ medium: null,
244
284
  minimal: null,
245
285
  off: "none",
246
286
  xhigh: "max",
@@ -280,7 +320,7 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
280
320
  thinkingLevelMap: {
281
321
  high: "high",
282
322
  low: "low",
283
- medium: "medium",
323
+ medium: null,
284
324
  minimal: null,
285
325
  off: "none",
286
326
  xhigh: "max",
@@ -398,12 +438,12 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
398
438
  maxTokens: 262144,
399
439
  reasoning: true,
400
440
  thinkingLevelMap: {
401
- high: "high",
402
- low: "low",
441
+ high: null,
442
+ low: null,
403
443
  medium: "medium",
404
444
  minimal: null,
405
445
  off: "none",
406
- xhigh: "max",
446
+ xhigh: null,
407
447
  },
408
448
  },
409
449
  {
@@ -438,12 +478,12 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
438
478
  maxTokens: 262144,
439
479
  reasoning: true,
440
480
  thinkingLevelMap: {
441
- high: "high",
442
- low: "low",
481
+ high: null,
482
+ low: null,
443
483
  medium: "medium",
444
484
  minimal: null,
445
485
  off: "none",
446
- xhigh: "max",
486
+ xhigh: null,
447
487
  },
448
488
  },
449
489
  {
@@ -480,7 +520,7 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
480
520
  thinkingLevelMap: {
481
521
  high: "high",
482
522
  low: "low",
483
- medium: "medium",
523
+ medium: null,
484
524
  minimal: null,
485
525
  off: "none",
486
526
  xhigh: "max",
@@ -518,12 +558,12 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
518
558
  maxTokens: 131072,
519
559
  reasoning: true,
520
560
  thinkingLevelMap: {
521
- high: "high",
522
- low: "low",
561
+ high: null,
562
+ low: null,
523
563
  medium: "medium",
524
564
  minimal: null,
525
565
  off: null,
526
- xhigh: "max",
566
+ xhigh: null,
527
567
  },
528
568
  },
529
569
  {
@@ -562,7 +602,7 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
562
602
  low: "low",
563
603
  medium: "medium",
564
604
  minimal: null,
565
- off: null,
605
+ off: "none",
566
606
  xhigh: "max",
567
607
  },
568
608
  },
@@ -630,12 +670,12 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
630
670
  maxTokens: 131072,
631
671
  reasoning: true,
632
672
  thinkingLevelMap: {
633
- high: "high",
634
- low: "low",
673
+ high: null,
674
+ low: null,
635
675
  medium: "medium",
636
676
  minimal: null,
637
677
  off: "none",
638
- xhigh: "max",
678
+ xhigh: null,
639
679
  },
640
680
  },
641
681
  {
@@ -670,12 +710,12 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
670
710
  maxTokens: 65536,
671
711
  reasoning: true,
672
712
  thinkingLevelMap: {
673
- high: "high",
674
- low: "low",
713
+ high: null,
714
+ low: null,
675
715
  medium: "medium",
676
716
  minimal: null,
677
717
  off: "none",
678
- xhigh: "max",
718
+ xhigh: null,
679
719
  },
680
720
  },
681
721
  {
@@ -710,12 +750,12 @@ export const GENERATED_MODELS: ProviderModelConfig[] = [
710
750
  maxTokens: 65536,
711
751
  reasoning: true,
712
752
  thinkingLevelMap: {
713
- high: "high",
714
- low: "low",
753
+ high: null,
754
+ low: null,
715
755
  medium: "medium",
716
756
  minimal: null,
717
757
  off: "none",
718
- xhigh: "max",
758
+ xhigh: null,
719
759
  },
720
760
  },
721
761
  {
package/models.ts CHANGED
@@ -93,7 +93,7 @@ function buildCompat(): ProviderModelConfig["compat"] {
93
93
  return {
94
94
  // Ollama uses "system" role, not "developer" (ollama: docs.ollama.com/api/openai-compatibility, pi: types.ts#supportsDeveloperRole).
95
95
  supportsDeveloperRole: false,
96
- // reasoning_effort works (ollama: docs.ollama.com/api/openai-compatibility, pi: types.ts#supportsReasoningEffort, tested in think-experiment.md).
96
+ // reasoning_effort works (ollama: docs.ollama.com/api/openai-compatibility, pi: types.ts#supportsReasoningEffort).
97
97
  supportsReasoningEffort: true,
98
98
  // "store" is not a supported field (ollama: docs.ollama.com/api/openai-compatibility, pi: types.ts#supportsStore).
99
99
  supportsStore: false,
@@ -109,7 +109,7 @@ function buildCompat(): ProviderModelConfig["compat"] {
109
109
  requiresThinkingAsText: false,
110
110
  // DeepSeek-specific, not needed for Ollama (pi: types.ts#requiresReasoningContentOnAssistantMessages).
111
111
  requiresReasoningContentOnAssistantMessages: false,
112
- // reasoning_effort format works (pi: types.ts#thinkingFormat, tested in think-experiment.md).
112
+ // reasoning_effort format works (pi: types.ts#thinkingFormat).
113
113
  thinkingFormat: "openai",
114
114
  // Ollama does not support tool_choice, so strict mode is unavailable (ollama: docs.ollama.com/api/openai-compatibility, pi: types.ts#supportsStrictMode).
115
115
  supportsStrictMode: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ollama-cloud",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -8,10 +8,12 @@
8
8
  "files": [
9
9
  "index.ts",
10
10
  "config.ts",
11
+ "cache.ts",
11
12
  "limits.generated.ts",
12
13
  "models.ts",
13
14
  "models.generated.ts",
14
15
  "pricing.generated.ts",
16
+ "reasoning.generated.ts",
15
17
  "thinking-levels.ts",
16
18
  "usage.ts",
17
19
  "utils.ts",
@@ -31,8 +33,9 @@
31
33
  "format": "biome format --write .",
32
34
  "test": "vitest run",
33
35
  "smoke:web-tools": "tsx scripts/smoke-web-tools.ts",
34
- "generate-models": "tsx scripts/generate-pricing.ts && tsx scripts/generate-models.ts && biome format --write models.generated.ts pricing.generated.ts",
35
- "generate-limits": "tsx scripts/generate-limits.ts && biome format --write limits.generated.ts"
36
+ "generate-models": "tsx scripts/generate-pricing.ts && tsx scripts/generate-reasoning.ts && tsx scripts/generate-models.ts && biome format --write models.generated.ts pricing.generated.ts reasoning.generated.ts",
37
+ "generate-limits": "tsx scripts/generate-limits.ts",
38
+ "generate-reasoning": "tsx scripts/generate-reasoning.ts && biome format --write reasoning.generated.ts"
36
39
  },
37
40
  "pi": {
38
41
  "extensions": [
@@ -50,6 +53,6 @@
50
53
  "@types/node": "^26.1.2",
51
54
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
52
55
  "tsx": "^4.19.0",
53
- "vitest": "^4.1.6"
56
+ "vitest": "^4.1.11"
54
57
  }
55
58
  }
@@ -1,7 +1,7 @@
1
1
  // Auto-generated by scripts/generate-pricing.ts
2
2
  // Do not edit manually.
3
- // Generated: 2026-09-03T10:12:00.135Z
4
- // Model count: 19
3
+ // Generated: 2026-09-12T02:41:55.206Z
4
+ // Model count: 20
5
5
 
6
6
  export interface ModelPrice {
7
7
  input: number;
@@ -13,6 +13,7 @@ export interface ModelPrice {
13
13
  export const MODEL_PRICING: Record<string, ModelPrice> = {
14
14
  "deepseek-v4-flash:0731": { input: 0.44, output: 1.32, cacheRead: 0.014, cacheWrite: 0 },
15
15
  "deepseek-v4-pro:0813": { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0 },
16
+ "deepseek-v4.1-flash": { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0 },
16
17
  "gemma4:31b": { input: 0.14, output: 0.4, cacheRead: 0.05, cacheWrite: 0 },
17
18
  "glm-5.1": { input: 1, output: 3.2, cacheRead: 0.2, cacheWrite: 0 },
18
19
  "glm-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },