pi-free 1.0.7 → 1.0.9

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
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.9] - 2026-04-14
11
+
12
+ ### Fixed
13
+ - **Qwen OAuth breaks other OAuth providers** — `modifyModels` receives all models across every registered provider, not just Qwen's. The previous `map()` stamped the Qwen dashscope `baseUrl` onto every model, causing other OAuth providers (Kilo, OpenRouter, etc.) to return 404 after a `/login qwen` flow. Now only models with `provider === PROVIDER_QWEN` are patched; others pass through unchanged.
14
+
15
+ ## [1.0.8] - 2026-04-13
16
+
17
+ ### Added
18
+ - **Modal provider** — Free access to GLM-5.1 FP8 (128k context, 16k max output) during promotional period (free until April 30, 2026)
19
+ - Requires a free Modal API key (`MODAL_API_KEY` or `modal_api_key` in `~/.pi/free.json`)
20
+ - Model: `zai-org/GLM-5.1-FP8` — 128k context window, 16k max output tokens
21
+ - **Qwen provider** — Free access to Qwen Coder (1,000 requests/day) via OAuth device flow
22
+ - Run `/login qwen` to authenticate through Qwen Studio (chat.qwen.ai)
23
+ - Uses `coder-model` alias (maps to Qwen3.6-Plus on the backend)
24
+ - 131k context window, 16k max output tokens, zero cost
25
+
26
+ ### Fixed
27
+ - **Qwen OAuth browser launch on Windows** — URLs with `&` query params were truncated by `cmd.exe`'s `&` command separator; switched to `powershell.exe Start-Process` which passes the URL as a literal string
28
+ - **Qwen API endpoint** — Replicates qwen-code's `getCurrentEndpoint()` logic: uses `resource_url` from OAuth token response (`dashscope.aliyuncs.com` for Chinese accounts, `portal.qwen.ai` for international), with fallback to `dashscope.aliyuncs.com/compatible-mode/v1`
29
+ - **Qwen DashScope headers** — Added all headers required by DashScope's OpenAI-compatible API: `X-DashScope-AuthType: qwen-oauth`, `X-DashScope-CacheControl: enable`, `X-DashScope-UserAgent`, `Client-Code: QwenCode`
30
+ - **Qwen modifyModels crash** — `modifyModels` must be synchronous; making it async caused the pi framework to receive a `Promise` instead of a `Model[]`, breaking `ModelRegistry.find()`
31
+
10
32
  ## [1.0.5] - 2025-04-03
11
33
 
12
34
  ### Fixed
package/README.md CHANGED
@@ -6,11 +6,11 @@ Free AI model providers for [Pi](https://pi.dev). Access **free models** from mu
6
6
 
7
7
  ## What does pi-free do
8
8
 
9
- **pi-free is a Pi extension that unlocks free AI models from 8 different providers.**
9
+ **pi-free is a Pi extension that unlocks free AI models from 10 different providers.**
10
10
 
11
11
  When you install pi-free, it:
12
12
 
13
- 1. **Registers 7 AI providers** with Pi's model picker — OpenCode Zen, Kilo, OpenRouter, NVIDIA NIM, Cline, Mistral, and Ollama Cloud
13
+ 1. **Registers 9 AI providers** with Pi's model picker — OpenCode Zen, Kilo, OpenRouter, NVIDIA NIM, Cline, Mistral, Ollama Cloud, Qwen, and Modal
14
14
 
15
15
  2. **Filters to show only free models by default** — You see only the models that cost $0 to use, no API key required for some providers
16
16
 
@@ -44,6 +44,8 @@ Free models are shown by default — look for the provider prefixes:
44
44
  - `cline/` — Cline models (run `/login cline` to use)
45
45
  - `mistral/` — Mistral models (API key required)
46
46
  - `ollama/` — Ollama Cloud models (API key required)
47
+ - `qwen/` — Qwen Coder (run `/login qwen` to use, 1,000 free requests/day)
48
+ - `modal/` — GLM-5.1 FP8 via Modal (API key required, free promotional period until April 30, 2026)
47
49
 
48
50
  ### 3. Toggle between free and paid models
49
51
 
@@ -77,7 +79,8 @@ Add your API keys to this file:
77
79
  "nvidia_api_key": "nvapi-...",
78
80
  "ollama_api_key": "...",
79
81
  "fireworks_api_key": "...",
80
- "mistral_api_key": "..."
82
+ "mistral_api_key": "...",
83
+ "modal_api_key": "sk-modal-..."
81
84
  }
82
85
  ```
83
86
 
@@ -92,8 +95,10 @@ See the [Providers That Need Authentication](#providers-that-need-authentication
92
95
  | `/{provider}-toggle` | Switch between free-only and all models for that provider |
93
96
  | `/login kilo` | Start OAuth flow for Kilo |
94
97
  | `/login cline` | Start OAuth flow for Cline |
98
+ | `/login qwen` | Start OAuth flow for Qwen |
95
99
  | `/logout kilo` | Clear Kilo OAuth credentials |
96
100
  | `/logout cline` | Clear Cline OAuth credentials |
101
+ | `/logout qwen` | Clear Qwen OAuth credentials |
97
102
 
98
103
  ---
99
104
 
@@ -225,6 +230,29 @@ This command will:
225
230
  - Free account required (no credit card)
226
231
  - Uses local ports 48801-48811 for OAuth callback
227
232
 
233
+ ### Modal (GLM-5.1 FP8 — free until April 30, 2026)
234
+
235
+ Modal hosts GLM-5.1 FP8 with a free promotional period. Get an API key at [modal.com](https://modal.com), then:
236
+
237
+ **Option A: Environment variable**
238
+ ```bash
239
+ export MODAL_API_KEY="sk-modal-..."
240
+ ```
241
+
242
+ **Option B: Config file** (`~/.pi/free.json`)
243
+ ```json
244
+ {
245
+ "modal_api_key": "sk-modal-..."
246
+ }
247
+ ```
248
+
249
+ Then select a `modal/` model in the model picker.
250
+
251
+ **Details:**
252
+ - Free promotional period until April 30, 2026
253
+ - Model: GLM-5.1 FP8 (128k context, 16k max output)
254
+ - No credit card required during the promotional period
255
+
228
256
  ### Mistral
229
257
 
230
258
  Add API key to `~/.pi/free.json` or environment variables:
@@ -233,6 +261,28 @@ Add API key to `~/.pi/free.json` or environment variables:
233
261
  export MISTRAL_API_KEY="..."
234
262
  ```
235
263
 
264
+ ### Qwen (1,000 free requests/day)
265
+
266
+ Qwen provides free access to **Qwen Coder** (Qwen3.6-Plus) via OAuth device flow — no API key or credit card needed.
267
+
268
+ ```
269
+ /login qwen
270
+ ```
271
+
272
+ This command will:
273
+ 1. Open your browser to Qwen Studio's authorization page
274
+ 2. Display a device code (enter it if the browser doesn't pre-fill it)
275
+ 3. Wait for you to authorize in the browser
276
+ 4. Automatically complete login once approved
277
+
278
+ Then select a `qwen/` model in the model picker.
279
+
280
+ **Details:**
281
+ - Free tier: 1,000 requests/day
282
+ - Model: Qwen Coder (131k context, 16k max output)
283
+ - No credit card required — just a free [Qwen Studio](https://chat.qwen.ai) account
284
+ - To re-authenticate: `/logout qwen` then `/login qwen`
285
+
236
286
  ---
237
287
 
238
288
  ## Slash Commands
@@ -248,6 +298,8 @@ Each provider has toggle commands to switch between free and all models:
248
298
  | `/cline-toggle` | Toggle between free/all Cline models |
249
299
  | `/mistral-toggle` | Toggle between free/all Mistral models |
250
300
  | `/ollama-toggle` | Toggle between free/all Ollama models |
301
+ | `/login qwen` | Authenticate with Qwen Studio (OAuth) |
302
+ | `/logout qwen` | Clear Qwen credentials |
251
303
 
252
304
  **The toggle command:**
253
305
  - Switches between showing only free models vs. all available models
@@ -269,6 +321,7 @@ Create `~/.pi/free.json` in your home directory:
269
321
  "opencode_api_key": "YOUR_ZEN_KEY",
270
322
  "ollama_api_key": "YOUR_OLLAMA_KEY",
271
323
  "ollama_show_paid": true,
324
+ "modal_api_key": "YOUR_MODAL_KEY",
272
325
  "hidden_models": ["model-id-to-hide"]
273
326
  }
274
327
  ```
@@ -282,6 +335,31 @@ export NVIDIA_API_KEY="..."
282
335
 
283
336
  ---
284
337
 
338
+ ## Logging & Debugging
339
+
340
+ pi-free now writes extension logs to:
341
+
342
+ - **Windows:** `%USERPROFILE%\.pi\free.log`
343
+ - **Linux/macOS:** `~/.pi/free.log`
344
+
345
+ Useful env vars:
346
+
347
+ ```bash
348
+ # Console log verbosity (default: error)
349
+ LOG_LEVEL=debug
350
+
351
+ # File log verbosity (default: debug)
352
+ PI_FREE_LOG_LEVEL=debug
353
+
354
+ # Custom log path (optional)
355
+ PI_FREE_LOG_PATH=/tmp/pi-free.log
356
+
357
+ # Disable file logging
358
+ PI_FREE_FILE_LOG=false
359
+ ```
360
+
361
+ ---
362
+
285
363
  ## License
286
364
 
287
365
  MIT — See [LICENSE](LICENSE)
package/config.ts CHANGED
@@ -6,10 +6,12 @@
6
6
  * 2. ~/.pi/free.json
7
7
  *
8
8
  * Per-provider paid model flags:
9
+ * KILO_SHOW_PAID=true or kilo_show_paid: true
9
10
  * OPENROUTER_SHOW_PAID=true or openrouter_show_paid: true
10
11
  * NVIDIA_SHOW_PAID=true or nvidia_show_paid: true
11
12
  * FIREWORKS_SHOW_PAID=true or fireworks_show_paid: true
12
13
  * CLINE_SHOW_PAID=true or cline_show_paid: true
14
+ * GO_SHOW_PAID=true or go_show_paid: true
13
15
  * OLLAMA_SHOW_PAID=true or ollama_show_paid: true
14
16
  *
15
17
  * PI_FREE_KILO_FREE_ONLY=true — restrict Kilo to free models even after login.
@@ -25,17 +27,21 @@ interface PiFreeConfig {
25
27
  openrouter_api_key?: string;
26
28
  nvidia_api_key?: string;
27
29
  opencode_api_key?: string;
30
+ opencode_go_api_key?: string;
28
31
  fireworks_api_key?: string;
29
32
  mistral_api_key?: string;
30
33
  ollama_api_key?: string;
34
+ modal_api_key?: string;
31
35
  kilo_free_only?: boolean;
32
36
  hidden_models?: string[];
33
37
  // Per-provider paid model flags
38
+ kilo_show_paid?: boolean;
34
39
  openrouter_show_paid?: boolean;
35
40
  nvidia_show_paid?: boolean;
36
41
  fireworks_show_paid?: boolean;
37
42
  cline_show_paid?: boolean;
38
43
  zen_show_paid?: boolean;
44
+ go_show_paid?: boolean;
39
45
  mistral_show_paid?: boolean;
40
46
  ollama_show_paid?: boolean;
41
47
  }
@@ -44,16 +50,20 @@ const CONFIG_TEMPLATE: PiFreeConfig = {
44
50
  openrouter_api_key: "",
45
51
  nvidia_api_key: "",
46
52
  opencode_api_key: "",
53
+ opencode_go_api_key: "",
47
54
  fireworks_api_key: "",
48
55
  mistral_api_key: "",
49
56
  ollama_api_key: "",
57
+ modal_api_key: "",
50
58
  kilo_free_only: false,
51
59
  hidden_models: [],
60
+ kilo_show_paid: false,
52
61
  openrouter_show_paid: false,
53
62
  nvidia_show_paid: false,
54
63
  fireworks_show_paid: false,
55
64
  cline_show_paid: false,
56
65
  zen_show_paid: false,
66
+ go_show_paid: false,
57
67
  mistral_show_paid: false,
58
68
  ollama_show_paid: false,
59
69
  };
@@ -123,6 +133,11 @@ function resolveBool(envKey: string, fileVal?: boolean): boolean {
123
133
  export const SHOW_PAID = process.env.PI_FREE_SHOW_PAID === "true";
124
134
 
125
135
  // Per-provider paid model flags - default to false (free-only) if not set
136
+ export const KILO_SHOW_PAID = resolveBool(
137
+ "KILO_SHOW_PAID",
138
+ file.kilo_show_paid,
139
+ );
140
+
126
141
  export const OPENROUTER_SHOW_PAID = resolveBool(
127
142
  "OPENROUTER_SHOW_PAID",
128
143
  file.openrouter_show_paid,
@@ -145,6 +160,8 @@ export const CLINE_SHOW_PAID = resolveBool(
145
160
 
146
161
  export const ZEN_SHOW_PAID = resolveBool("ZEN_SHOW_PAID", file.zen_show_paid);
147
162
 
163
+ export const GO_SHOW_PAID = resolveBool("GO_SHOW_PAID", file.go_show_paid);
164
+
148
165
  export const MISTRAL_SHOW_PAID = resolveBool(
149
166
  "MISTRAL_SHOW_PAID",
150
167
  file.mistral_show_paid,
@@ -177,23 +194,31 @@ export const OPENCODE_API_KEY = resolve(
177
194
  "OPENCODE_API_KEY",
178
195
  file.opencode_api_key,
179
196
  );
197
+ export const OPENCODE_GO_API_KEY = resolve(
198
+ "OPENCODE_GO_API_KEY",
199
+ file.opencode_go_api_key,
200
+ );
180
201
  export const FIREWORKS_API_KEY = resolve(
181
202
  "FIREWORKS_API_KEY",
182
203
  file.fireworks_api_key,
183
204
  );
184
205
  export const MISTRAL_API_KEY = resolve("MISTRAL_API_KEY", file.mistral_api_key);
185
206
  export const OLLAMA_API_KEY = resolve("OLLAMA_API_KEY", file.ollama_api_key);
207
+ export const MODAL_API_KEY = resolve("MODAL_API_KEY", file.modal_api_key);
186
208
 
187
209
  // Re-export provider names for consistency
188
210
  export {
189
211
  PROVIDER_CLINE,
190
212
  PROVIDER_FIREWORKS,
213
+ PROVIDER_GO,
191
214
  PROVIDER_KILO,
192
215
  PROVIDER_MISTRAL,
193
216
  PROVIDER_NVIDIA,
194
217
  PROVIDER_OLLAMA,
195
218
  PROVIDER_OPENROUTER,
219
+ PROVIDER_QWEN,
196
220
  PROVIDER_ZEN,
221
+ PROVIDER_MODAL,
197
222
  } from "./constants.ts";
198
223
 
199
224
  // =============================================================================
package/constants.ts CHANGED
@@ -9,20 +9,28 @@
9
9
 
10
10
  export const PROVIDER_KILO = "kilo";
11
11
  export const PROVIDER_ZEN = "zen";
12
+ export const PROVIDER_GO = "go";
12
13
  export const PROVIDER_OPENROUTER = "openrouter";
13
14
  export const PROVIDER_NVIDIA = "nvidia";
14
15
  export const PROVIDER_CLINE = "cline";
15
16
  export const PROVIDER_FIREWORKS = "fireworks";
16
17
  export const PROVIDER_OLLAMA = "ollama";
18
+ export const PROVIDER_MISTRAL = "mistral";
19
+ export const PROVIDER_QWEN = "qwen";
20
+ export const PROVIDER_MODAL = "modal";
17
21
 
18
22
  export const ALL_PROVIDERS = [
19
23
  PROVIDER_KILO,
20
24
  PROVIDER_ZEN,
25
+ PROVIDER_GO,
21
26
  PROVIDER_OPENROUTER,
22
27
  PROVIDER_NVIDIA,
23
28
  PROVIDER_CLINE,
24
29
  PROVIDER_FIREWORKS,
30
+ PROVIDER_MISTRAL,
25
31
  PROVIDER_OLLAMA,
32
+ PROVIDER_QWEN,
33
+ PROVIDER_MODAL,
26
34
  ] as const;
27
35
 
28
36
  // =============================================================================
@@ -31,11 +39,13 @@ export const ALL_PROVIDERS = [
31
39
 
32
40
  export const BASE_URL_KILO = "https://api.kilo.ai/api/gateway";
33
41
  export const BASE_URL_ZEN = "https://opencode.ai/zen/v1";
42
+ export const BASE_URL_GO = "https://opencode.ai/zen/go/v1";
34
43
  export const BASE_URL_OPENROUTER = "https://openrouter.ai/api/v1";
35
44
  export const BASE_URL_NVIDIA = "https://integrate.api.nvidia.com/v1";
36
45
  export const BASE_URL_CLINE = "https://api.cline.bot/api/v1";
37
46
  export const BASE_URL_FIREWORKS = "https://api.fireworks.ai/inference/v1";
38
47
  export const BASE_URL_OLLAMA = "https://ollama.com/v1";
48
+ export const BASE_URL_MODAL = "https://api.us-west-2.modal.direct/v1";
39
49
 
40
50
  // =============================================================================
41
51
  // External URLs
@@ -44,7 +54,11 @@ export const BASE_URL_OLLAMA = "https://ollama.com/v1";
44
54
  export const URL_MODELS_DEV = "https://models.dev/api.json";
45
55
  export const URL_KILO_TOS = "https://kilo.ai/terms";
46
56
  export const URL_ZEN_TOS = "https://opencode.ai/terms";
57
+ export const URL_GO_TOS = "https://opencode.ai/terms";
47
58
  export const URL_CLINE_TOS = "https://cline.bot/tos";
59
+ export const URL_QWEN_TOS = "https://terms.alicloud.com/";
60
+ export const URL_MODAL_TOS = "https://modal.com/terms";
61
+ export const BASE_URL_QWEN = "https://dashscope.aliyuncs.com/compatible-mode/v1";
48
62
 
49
63
  // =============================================================================
50
64
  // Cline auth
@@ -98,7 +112,6 @@ export const KILO_TOKEN_EXPIRATION_MS = 365 * 24 * 60 * 60 * 1000; // 1 year
98
112
  export const PROVIDER_GROQ = "groq";
99
113
  export const PROVIDER_TOGETHER = "together";
100
114
  export const PROVIDER_DEEPINFRA = "deepinfra";
101
- export const PROVIDER_MISTRAL = "mistral";
102
115
  export const PROVIDER_PERPLEXITY = "perplexity";
103
116
  export const PROVIDER_XAI = "xai";
104
117
 
package/lib/logger.ts CHANGED
@@ -1,8 +1,16 @@
1
1
  /**
2
2
  * Structured logging utility.
3
3
  * Replaces console.log statements with namespaced, level-based logging.
4
+ *
5
+ * File logging:
6
+ * - Default file: ~/.pi/free.log
7
+ * - Override path: PI_FREE_LOG_PATH=/custom/path/free.log
8
+ * - Disable file logging: PI_FREE_FILE_LOG=false
4
9
  */
5
10
 
11
+ import { appendFileSync, existsSync, mkdirSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+
6
14
  export type LogLevel = "debug" | "info" | "warn" | "error";
7
15
 
8
16
  interface LogEntry {
@@ -20,25 +28,55 @@ const LOG_LEVELS: Record<LogLevel, number> = {
20
28
  error: 3,
21
29
  };
22
30
 
23
- // Default to error-only. Set LOG_LEVEL=debug or LOG_LEVEL=info to see more.
31
+ // Console default: error-only. Set LOG_LEVEL=debug or LOG_LEVEL=info to see more.
24
32
  const currentLevel: LogLevel = (process.env.LOG_LEVEL as LogLevel) || "error";
33
+ // File default: debug (so we can inspect full behavior in ~/.pi/free.log).
34
+ const fileLevel: LogLevel =
35
+ (process.env.PI_FREE_LOG_LEVEL as LogLevel) || "debug";
25
36
 
26
- function shouldLog(level: LogLevel): boolean {
27
- return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
37
+ function shouldLog(level: LogLevel, minLevel: LogLevel): boolean {
38
+ return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
28
39
  }
29
40
 
30
41
  function formatMessage(entry: LogEntry): string {
31
- const data = entry.data ? ` ${JSON.stringify(entry.data)}` : "";
42
+ let data = "";
43
+ if (entry.data) {
44
+ try {
45
+ data = ` ${JSON.stringify(entry.data)}`;
46
+ } catch {
47
+ data = " [unserializable-data]";
48
+ }
49
+ }
32
50
  return `[${entry.timestamp}] [${entry.level.toUpperCase()}] [${entry.namespace}] ${entry.message}${data}`;
33
51
  }
34
52
 
53
+ const HOME_DIR = process.env.HOME || process.env.USERPROFILE || "";
54
+ const DEFAULT_LOG_PATH = join(HOME_DIR, ".pi", "free.log");
55
+ const LOG_PATH = process.env.PI_FREE_LOG_PATH || DEFAULT_LOG_PATH;
56
+ const FILE_LOG_ENABLED = process.env.PI_FREE_FILE_LOG !== "false";
57
+
58
+ function appendToFile(line: string): void {
59
+ if (!FILE_LOG_ENABLED) return;
60
+ try {
61
+ const dir = dirname(LOG_PATH);
62
+ if (!existsSync(dir)) {
63
+ mkdirSync(dir, { recursive: true });
64
+ }
65
+ appendFileSync(LOG_PATH, `${line}\n`, "utf8");
66
+ } catch {
67
+ // Never throw from logger
68
+ }
69
+ }
70
+
35
71
  function log(
36
72
  level: LogLevel,
37
73
  namespace: string,
38
74
  message: string,
39
75
  data?: Record<string, unknown>,
40
76
  ): void {
41
- if (!shouldLog(level)) return;
77
+ const logToConsole = shouldLog(level, currentLevel);
78
+ const logToFile = shouldLog(level, fileLevel);
79
+ if (!logToConsole && !logToFile) return;
42
80
 
43
81
  const entry: LogEntry = {
44
82
  timestamp: new Date().toISOString(),
@@ -49,6 +87,13 @@ function log(
49
87
  };
50
88
 
51
89
  const formatted = formatMessage(entry);
90
+ if (logToFile) {
91
+ appendToFile(formatted);
92
+ }
93
+
94
+ if (!logToConsole) {
95
+ return;
96
+ }
52
97
 
53
98
  switch (level) {
54
99
  case "debug":