pi-ollama-cloud 0.2.1 → 0.3.1

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,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.3.1] - 2026-05-05
6
+
7
+ - Fix `OLLAMA_API_KEY` env var not being respected by `fetchModels` and web tools. pi-ai does not know about the `ollama-cloud` provider ID, so `AuthStorage.getApiKey()` alone misses the env var. Added explicit `process.env.OLLAMA_API_KEY` fallback.
8
+ - Switch web tools to `AuthStorage.create()` for API key lookup, matching the `models.ts` auth pattern from v0.2.1.
9
+ - Add null-safe access to `data.details?.family` in `resolveThinkingLevelMap`.
10
+ - Change `OLLAMA_BASE` from `export let` to `export const` to prevent accidental mutation.
11
+ - Fix fallback model IDs to use real Ollama Cloud identifiers (`glm-5.1`, `gemma4:31b`) instead of synthetic `:cloud` suffixes.
12
+ - Add smoke test workflow for CI.
13
+
14
+ ## [0.3.0] - 2026-05-04
15
+
16
+ - Derive `thinkingLevelMap` from pi's built-in model definitions instead of hardcoding model-family mappings. The extension now picks up thinking level metadata automatically when pi-mono adds or updates it for any model.
17
+ - Add family-based fallback matching: when an Ollama Cloud model ID doesn't match a pi model ID exactly, the extension now tries matching by model family (via Ollama's `details.family` field). For example, `gemma4:31b` correctly picks up Gemma 4's thinking level map from pi.
18
+
5
19
  ## [0.2.1] - 2026-04-29
6
20
 
7
21
  - Fix API key retrieval by using `AuthStorage` instead of `ctx.modelRegistry.getApiKeyForProvider`. The provider-level API key lookup was failing, causing auth to only work when an environment variable was set. Now reads from `auth.json` directly via the pi `AuthStorage` class.
package/models.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { type ExtensionCommandContext, getAgentDir, type ProviderModelConfig } from "@mariozechner/pi-coding-agent";
4
- import { AuthStorage } from "@mariozechner/pi-coding-agent";
3
+ import { getModels, getProviders } from "@mariozechner/pi-ai";
4
+ import {
5
+ AuthStorage,
6
+ type ExtensionCommandContext,
7
+ getAgentDir,
8
+ type ProviderModelConfig,
9
+ } from "@mariozechner/pi-coding-agent";
5
10
 
6
11
  // --- Constants ---
7
12
  const CACHE_DIR = join(getAgentDir(), "cache");
@@ -9,7 +14,7 @@ const CACHE_FILE = join(CACHE_DIR, "ollama-cloud-models.json");
9
14
  const FETCH_TIMEOUT_MS = 10000;
10
15
 
11
16
  // --- API fetch ---
12
- export let OLLAMA_BASE = (process.env.OLLAMA_API_BASE || "https://ollama.com").replace(/\/+$/, "");
17
+ export const OLLAMA_BASE = (process.env.OLLAMA_API_BASE || "https://ollama.com").replace(/\/+$/, "");
13
18
 
14
19
  // Initialize AuthStorage
15
20
  const authStorage = AuthStorage.create();
@@ -46,6 +51,54 @@ function getContextLength(modelInfo: Record<string, unknown>): number {
46
51
  return 128000;
47
52
  }
48
53
 
54
+ // --- Built-in model knowledge index ---
55
+ // Build a lookup of model ID -> thinkingLevelMap from pi's built-in models.
56
+ // This avoids hardcoding model-family mappings: when pi-mono updates its
57
+ // model definitions (e.g. DeepSeek V4's thinking levels), the extension
58
+ // picks up the changes automatically.
59
+ const BUILTIN_THINKING_MAP: Record<string, ProviderModelConfig["thinkingLevelMap"]> = {};
60
+ // Fallback: family stem -> [stem, thinkingLevelMap] pairs for models whose Ollama Cloud
61
+ // ID doesn't match exactly. The stem is derived by stripping provider prefixes and
62
+ // non-alphanumeric characters (e.g. "gemma-4-31b-it" -> "gemma431bit").
63
+ // When looking up an Ollama model by its details.family field, we search for a pi stem
64
+ // that starts with the family stem (e.g. family "gemma4" -> pi "gemma431bit").
65
+ // Entries are sorted longest-first so the most specific match wins.
66
+ const BUILTIN_FAMILY_ENTRIES: [string, NonNullable<ProviderModelConfig["thinkingLevelMap"]>][] = [];
67
+ for (const provider of getProviders()) {
68
+ for (const model of getModels(provider as any)) {
69
+ if (model.thinkingLevelMap) {
70
+ BUILTIN_THINKING_MAP[model.id] = model.thinkingLevelMap;
71
+ const stem = model.id
72
+ .replace(/^[a-z0-9-]+\//, "") // strip provider prefix (e.g. "zai/", "deepseek/")
73
+ .replace(/[^a-zA-Z0-9]/g, "") // strip non-alphanumeric
74
+ .toLowerCase();
75
+ BUILTIN_FAMILY_ENTRIES.push([stem, model.thinkingLevelMap]);
76
+ }
77
+ }
78
+ }
79
+ // Longest stems first so a more specific match (e.g. "gemma431bit") wins over a generic one (e.g. "gemma4").
80
+ BUILTIN_FAMILY_ENTRIES.sort((a, b) => b[0].length - a[0].length);
81
+
82
+ function resolveThinkingLevelMap(modelId: string, data: OllamaShowResponse): ProviderModelConfig["thinkingLevelMap"] {
83
+ // 1. Exact ID match (e.g. "deepseek-v4-pro")
84
+ const exact = BUILTIN_THINKING_MAP[modelId];
85
+ if (exact) return exact;
86
+
87
+ // 2. Family-based fallback: match Ollama's details.family against pi model stems
88
+ if (data.capabilities?.includes("thinking")) {
89
+ const familyStem = data.details?.family?.replace(/[^a-zA-Z0-9]/g, "").toLowerCase() ?? "";
90
+ if (familyStem) {
91
+ for (const [stem, tlm] of BUILTIN_FAMILY_ENTRIES) {
92
+ if (stem.startsWith(familyStem)) {
93
+ return tlm;
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ return undefined;
100
+ }
101
+
49
102
  export function assembleModels(raw: Record<string, OllamaShowResponse>): ProviderModelConfig[] {
50
103
  return Object.entries(raw)
51
104
  .filter(([, data]) => data.capabilities?.includes("tools"))
@@ -53,6 +106,7 @@ export function assembleModels(raw: Record<string, OllamaShowResponse>): Provide
53
106
  id,
54
107
  name: id,
55
108
  reasoning: data.capabilities?.includes("thinking") ?? false,
109
+ thinkingLevelMap: resolveThinkingLevelMap(id, data),
56
110
  input: (data.capabilities?.includes("vision") ? ["text", "image"] : ["text"]) as ("text" | "image")[],
57
111
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
58
112
  contextWindow: getContextLength(data.model_info ?? {}),
@@ -63,18 +117,18 @@ export function assembleModels(raw: Record<string, OllamaShowResponse>): Provide
63
117
  // --- Fallback models (cold cache) ---
64
118
  export const FALLBACK_MODELS: ProviderModelConfig[] = [
65
119
  {
66
- id: "glm-5.1:cloud",
67
- name: "GLM 5.1 Cloud",
68
- reasoning: true,
120
+ id: "glm-5.1",
121
+ name: "GLM 5.1",
122
+ reasoning: false,
69
123
  input: ["text"],
70
124
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
71
125
  contextWindow: 202752,
72
126
  maxTokens: 32768,
73
127
  },
74
128
  {
75
- id: "gemma4:cloud",
76
- name: "Gemma 4 Cloud",
77
- reasoning: true,
129
+ id: "gemma4:31b",
130
+ name: "Gemma 4 31B",
131
+ reasoning: false,
78
132
  input: ["text"],
79
133
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
80
134
  contextWindow: 262144,
@@ -105,17 +159,18 @@ export function writeCache(models: Record<string, OllamaShowResponse>): void {
105
159
 
106
160
  // --- Fetch Models ---
107
161
  export async function fetchModels(ctx: ExtensionCommandContext): Promise<Record<string, OllamaShowResponse> | null> {
108
- const apiKey = await authStorage.getApiKey("ollama-cloud");
109
-
162
+ const apiKey = (await authStorage.getApiKey("ollama-cloud")) ?? process.env.OLLAMA_API_KEY;
163
+
110
164
  if (!apiKey) {
111
165
  ctx.ui.notify(
112
166
  "No Ollama Cloud API key found. \n" +
113
- "Please ensure your API key is set in: \n" +
114
- "- auth.json file (at ~/.pi/agent/auth.json) under 'ollama-cloud' key,\n" +
115
- "- or via the CLI --api-key flag.\n" +
116
- "Example auth.json entry: \n" +
117
- '{ \"ollama-cloud\": { \"type\": \"api_key\", \"key\": \"YOUR_API_KEY\" } }'
118
- , "error");
167
+ "Please ensure your API key is set in: \n" +
168
+ "- auth.json file (at ~/.pi/agent/auth.json) under 'ollama-cloud' key,\n" +
169
+ "- or via the CLI --api-key flag.\n" +
170
+ "Example auth.json entry: \n" +
171
+ '{ "ollama-cloud": { "type": "api_key", "key": "YOUR_API_KEY" } }',
172
+ "error",
173
+ );
119
174
  return null;
120
175
  }
121
176
 
@@ -176,4 +231,4 @@ export async function fetchModels(ctx: ExtensionCommandContext): Promise<Record<
176
231
  ctx.ui.notify(`Fetched ${succeeded} model details${failed ? ` (${failed} failed)` : ""}`, "info");
177
232
 
178
233
  return results;
179
- }
234
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ollama-cloud",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"
package/web-tools.ts CHANGED
@@ -1,9 +1,4 @@
1
- import {
2
- type ExtensionAPI,
3
- type ExtensionContext,
4
- keyHint,
5
- truncateToVisualLines,
6
- } from "@mariozechner/pi-coding-agent";
1
+ import { AuthStorage, type ExtensionAPI, keyHint, truncateToVisualLines } from "@mariozechner/pi-coding-agent";
7
2
  import { Text, truncateToWidth } from "@mariozechner/pi-tui";
8
3
  import { Type } from "@sinclair/typebox";
9
4
  import { OLLAMA_BASE } from "./models.ts";
@@ -26,8 +21,10 @@ interface FetchResponse {
26
21
 
27
22
  // --- Helpers ---
28
23
 
29
- async function getCloudApiKey(ctx: ExtensionContext): Promise<string | undefined> {
30
- return ctx.modelRegistry.getApiKeyForProvider("ollama-cloud");
24
+ const authStorage = AuthStorage.create();
25
+
26
+ async function getCloudApiKey(): Promise<string | undefined> {
27
+ return authStorage.getApiKey("ollama-cloud") ?? process.env.OLLAMA_API_KEY;
31
28
  }
32
29
 
33
30
  function noApiKeyError() {
@@ -121,8 +118,8 @@ export function registerWebSearchTool(pi: ExtensionAPI) {
121
118
  }),
122
119
  ),
123
120
  }),
124
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
125
- const apiKey = await getCloudApiKey(ctx);
121
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
122
+ const apiKey = await getCloudApiKey();
126
123
  if (!apiKey) return noApiKeyError();
127
124
 
128
125
  try {
@@ -180,8 +177,8 @@ export function registerWebFetchTool(pi: ExtensionAPI) {
180
177
  parameters: Type.Object({
181
178
  url: Type.String({ description: "URL to fetch and extract content from", format: "uri" }),
182
179
  }),
183
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
184
- const apiKey = await getCloudApiKey(ctx);
180
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
181
+ const apiKey = await getCloudApiKey();
185
182
  if (!apiKey) return noApiKeyError();
186
183
 
187
184
  try {