pi-freeflow 1.3.3 → 1.3.6

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/src/models.ts CHANGED
@@ -305,9 +305,57 @@ export const KILO_MODELS: ModelDef[] = [
305
305
  ];
306
306
 
307
307
  /**
308
- * Set of all KiloCode model IDs for fast lookup
308
+ * Model ID Aliases — maps user-friendly / slash-free CLI IDs to canonical upstream model IDs.
309
309
  */
310
- export const KILO_MODEL_IDS = new Set<string>(KILO_MODELS.map((m) => m.id));
310
+ export const MODEL_ALIASES: Record<string, string> = {
311
+ // Kilo Gateway slash-free & colon-free CLI aliases
312
+ "dots-3-note-preview": "dots-studio/dots-3-note-preview:free",
313
+ "dots-3-note-preview:free": "dots-studio/dots-3-note-preview:free",
314
+ "step-3.7-flash": "stepfun/step-3.7-flash:free",
315
+ "step-3.7-flash:free": "stepfun/step-3.7-flash:free",
316
+ "nemotron-3-nano-omni": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
317
+ "nemotron-3-nano-omni:free": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
318
+ "nemotron-3-ultra-550b": "nvidia/nemotron-3-ultra-550b-a55b:free",
319
+ "nemotron-3-ultra-550b:free": "nvidia/nemotron-3-ultra-550b-a55b:free",
320
+ "nemotron-3-super": "nvidia/nemotron-3-super-120b-a12b:free",
321
+ "nemotron-3-super:free": "nvidia/nemotron-3-super-120b-a12b:free",
322
+ "hy3:free": "tencent/hy3:free",
323
+ "north-mini-code": "cohere/north-mini-code:free",
324
+ "north-mini-code:free": "cohere/north-mini-code:free",
325
+ "laguna-s-2.1:free": "poolside/laguna-s-2.1:free",
326
+ "laguna-xs-2.1:free": "poolside/laguna-xs-2.1:free",
327
+ "lfm-2.5": "liquid/lfm-2.5-2.6b:free",
328
+ "lfm-2.5:free": "liquid/lfm-2.5-2.6b:free",
329
+ "content-safety": "nvidia/nemotron-3.5-content-safety:free",
330
+ "content-safety:free": "nvidia/nemotron-3.5-content-safety:free",
331
+ "kilo-auto": "kilo-auto/free",
332
+ "openrouter": "openrouter/free",
333
+
334
+ // OpenCode Zen aliases
335
+ "claude-sonnet-4.5-free": "muse-spark-1.2-contributor-free",
336
+ "claude-sonnet-4.5-contributor-free": "muse-spark-1.2-contributor-free",
337
+ "grok-code-fast-1-preview-f-free": "x-preview-f-free",
338
+ "minimax-m2.1-free": "laguna-s-2.1-free",
339
+ "qwen3-coder-480b-free": "hy3-free",
340
+ };
341
+
342
+ /**
343
+ * Resolve any model alias to its canonical upstream model ID.
344
+ */
345
+ export function resolveCanonicalModelId(id: string): string {
346
+ const clean = (id || "").trim();
347
+ return MODEL_ALIASES[clean] || clean;
348
+ }
349
+
350
+ /**
351
+ * Set of all KiloCode model IDs (including aliases) for fast lookup
352
+ */
353
+ export const KILO_MODEL_IDS = new Set<string>([
354
+ ...KILO_MODELS.map((m) => m.id),
355
+ ...Object.entries(MODEL_ALIASES)
356
+ .filter(([_, target]) => KILO_MODELS.some((km) => km.id === target))
357
+ .map(([alias]) => alias),
358
+ ]);
311
359
 
312
360
  /**
313
361
  * Combined list of all 23 static free models
@@ -317,22 +365,38 @@ export const ALL_MODELS: ModelDef[] = [...OPENCODE_MODELS, ...KILO_MODELS];
317
365
  /**
318
366
  * Map of model ID -> ModelDef
319
367
  */
320
- export const MODEL_MAP = new Map<string, ModelDef>(
321
- ALL_MODELS.map((m) => [m.id, m]),
322
- );
323
-
368
+ export const MODEL_MAP = new Map<string, ModelDef>([
369
+ ...ALL_MODELS.map((m): [string, ModelDef] => [m.id, m]),
370
+ ...Object.entries(MODEL_ALIASES).map(([aliasId, canonicalId]): [string, ModelDef] => {
371
+ const base = ALL_MODELS.find((m) => m.id === canonicalId);
372
+ return [
373
+ aliasId,
374
+ base
375
+ ? { ...base, id: aliasId }
376
+ : { id: aliasId, name: aliasId, reasoning: false, contextWindow: 200_000, maxTokens: 32_000, input: ["text"] },
377
+ ];
378
+ }),
379
+ ]);
324
380
  /**
325
381
  * Lookup a model definition by ID
326
382
  */
327
383
  export function getModelDef(id: string): ModelDef | undefined {
328
- return MODEL_MAP.get(id);
384
+ return MODEL_MAP.get(id) || MODEL_MAP.get(resolveCanonicalModelId(id));
385
+ }
386
+ /**
387
+ * Get full list of registered models including CLI aliases
388
+ */
389
+ export function getAllRegisteredModels(): ModelDef[] {
390
+ return Array.from(MODEL_MAP.values());
329
391
  }
330
392
 
393
+
331
394
  /**
332
395
  * Check if a model ID belongs to KiloCode Gateway
333
396
  */
334
397
  export function isKiloModel(id: string): boolean {
335
- return KILO_MODEL_IDS.has(id);
398
+ const canonical = resolveCanonicalModelId(id);
399
+ return KILO_MODEL_IDS.has(id) || KILO_MODEL_IDS.has(canonical);
336
400
  }
337
401
 
338
402
  /**
package/src/proxy.ts CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  opencodeHeaders,
24
24
  } from "./config.ts";
25
25
  import { isDebugEnabled, log } from "./logger.ts";
26
- import { KILO_MODEL_IDS } from "./models.ts";
26
+ import { KILO_MODEL_IDS, resolveCanonicalModelId } from "./models.ts";
27
27
  // normalize removed — host pi-ai already normalizes thinking/reasoning before proxy
28
28
  import { checkRateLimit } from "./rate-limiter.ts";
29
29
  import { relayFetch } from "./relay.ts";
@@ -87,6 +87,23 @@ export function sanitizeHeaders(
87
87
  return sanitized;
88
88
  }
89
89
 
90
+ /**
91
+ * Clamps reasoning_effort for upstream models with strict non-standard enums
92
+ * (e.g. OpenCode x-preview strictly requires 'low', 'high', or 'max' and rejects 'medium' with 400).
93
+ */
94
+ function sanitizeReasoningForModel(bodyObj: Record<string, unknown>): void {
95
+ const model = String(bodyObj.model || "").toLowerCase();
96
+ if (model.includes("x-preview")) {
97
+ const effort = String(bodyObj.reasoning_effort || "").toLowerCase();
98
+ if (effort === "medium") {
99
+ bodyObj.reasoning_effort = "high";
100
+ } else if (effort === "minimal") {
101
+ bodyObj.reasoning_effort = "low";
102
+ } else if (!effort || effort === "off" || effort === "none") {
103
+ bodyObj.reasoning_effort = "low";
104
+ }
105
+ }
106
+ }
90
107
  /**
91
108
  * Probe whether an existing pi-freeflow proxy daemon is running and responsive on a given port.
92
109
  */
@@ -198,11 +215,12 @@ export function startProxy(
198
215
 
199
216
  try {
200
217
  parsedBody = JSON.parse(bodyStr);
201
- if (
202
- typeof parsedBody?.model === "string" &&
203
- KILO_MODEL_IDS.has(parsedBody.model)
204
- ) {
205
- isKilo = true;
218
+ if (typeof parsedBody?.model === "string") {
219
+ const canonical = resolveCanonicalModelId(parsedBody.model);
220
+ parsedBody.model = canonical;
221
+ if (KILO_MODEL_IDS.has(canonical)) {
222
+ isKilo = true;
223
+ }
206
224
  }
207
225
  } catch {}
208
226
 
@@ -266,10 +284,11 @@ export function startProxy(
266
284
  } else {
267
285
  // OpenCode routing — relay when enabled, else direct upstream
268
286
  const relayState = getActiveRelayState();
269
- if (
270
- relayState.enabled &&
271
- (relayState.url || relayState.relays.length > 0)
272
- ) {
287
+ const shouldUseRelay =
288
+ relayState.mode !== "off" &&
289
+ relayState.enabled !== false &&
290
+ Boolean(relayState.url || (relayState.relays && relayState.relays.length > 0));
291
+ if (shouldUseRelay) {
273
292
  const fullUrl = `${UPSTREAM_OPENCODE}${req.url ?? "/"}`;
274
293
  const activeHost = relayState.url
275
294
  ? new URL(relayState.url).host
@@ -279,6 +298,7 @@ export function startProxy(
279
298
  try {
280
299
  if (parsedBody) {
281
300
  const relayBodyObj = structuredClone(parsedBody);
301
+ sanitizeReasoningForModel(relayBodyObj as Record<string, unknown>);
282
302
  const relayBody = Buffer.from(JSON.stringify(relayBodyObj));
283
303
  const response = await relayFetch(
284
304
  fullUrl,
@@ -334,6 +354,7 @@ export function startProxy(
334
354
  let directBody = Buffer.concat(bodyChunks);
335
355
  if (parsedBody) {
336
356
  const directBodyObj = structuredClone(parsedBody);
357
+ sanitizeReasoningForModel(directBodyObj as Record<string, unknown>);
337
358
  directBody = Buffer.from(JSON.stringify(directBodyObj));
338
359
  }
339
360
 
@@ -9,27 +9,35 @@ import fs from "node:fs";
9
9
  import path from "node:path";
10
10
  import { DEFAULT_RELAY_URL, RELAY_STATE_FILE } from "./config.ts";
11
11
  import { logWarn } from "./logger.ts";
12
- import type { ExtensionUIContext, KnownRelay, RelayState } from "./types.ts";
13
-
12
+ import type { ExtensionUIContext, KnownRelay, RelayMode, RelayState } from "./types.ts";
14
13
  /**
15
14
  * Load persisted relay state from disk.
16
15
  */
17
16
  export function loadRelayState(): RelayState {
18
17
  try {
19
18
  if (!fs.existsSync(RELAY_STATE_FILE)) {
20
- return { enabled: true, url: DEFAULT_RELAY_URL, relays: [] };
19
+ return { mode: "auto", enabled: true, url: DEFAULT_RELAY_URL, relays: [] };
21
20
  }
22
21
  const s = JSON.parse(fs.readFileSync(RELAY_STATE_FILE, "utf8"));
23
22
  const relays: KnownRelay[] = Array.isArray(s?.relays) ? s.relays : [];
24
- // Auto-on by default if saved relays exist, unless explicitly set to false
25
- const enabled = s?.enabled !== undefined ? Boolean(s.enabled) : relays.length > 0;
23
+ const mode: RelayMode =
24
+ s?.mode === "on" || s?.mode === "off" || s?.mode === "auto"
25
+ ? s.mode
26
+ : "auto";
27
+ const enabled =
28
+ mode === "on"
29
+ ? true
30
+ : mode === "off"
31
+ ? false
32
+ : s?.enabled !== false && relays.length > 0;
26
33
  return {
34
+ mode,
27
35
  enabled,
28
36
  url: typeof s?.url === "string" ? s.url.trim() : (relays[0]?.url || DEFAULT_RELAY_URL),
29
37
  relays,
30
38
  };
31
39
  } catch {
32
- return { enabled: true, url: DEFAULT_RELAY_URL, relays: [] };
40
+ return { mode: "auto", enabled: true, url: DEFAULT_RELAY_URL, relays: [] };
33
41
  }
34
42
  }
35
43
 
@@ -51,13 +59,83 @@ export function saveRelayState(s: RelayState): void {
51
59
  }
52
60
 
53
61
  /**
54
- * Deduplicate and add a relay URL to the known relay list.
62
+ * Deduplicate and add or update a relay URL in the known relay list.
55
63
  */
56
- export function ensureRelay(s: RelayState, url: string, label?: string): void {
57
- if (!url || s.relays.some((r) => r.url === url)) {
58
- return;
64
+ export function ensureRelay(
65
+ s: RelayState,
66
+ url: string,
67
+ label?: string,
68
+ ): KnownRelay {
69
+ const cleanUrl = (url || "").trim();
70
+ if (!cleanUrl) {
71
+ throw new Error("Relay URL cannot be empty");
72
+ }
73
+ const cleanLabel = (label || "").trim() || undefined;
74
+ const existing = s.relays.find((r) => r.url === cleanUrl);
75
+ if (existing) {
76
+ if (cleanLabel && cleanLabel !== "manual") {
77
+ existing.label = cleanLabel;
78
+ }
79
+ return existing;
80
+ }
81
+ const newRelay: KnownRelay = {
82
+ url: cleanUrl,
83
+ label: cleanLabel && cleanLabel !== "manual" ? cleanLabel : undefined,
84
+ addedAt: new Date().toISOString(),
85
+ };
86
+ s.relays.push(newRelay);
87
+ return newRelay;
88
+ }
89
+
90
+ /**
91
+ * Set or update short name / label for a relay by URL, index, or existing label.
92
+ */
93
+ export function setRelayLabel(
94
+ s: RelayState,
95
+ identifier: string | number,
96
+ label: string,
97
+ ): KnownRelay | null {
98
+ const relay = findRelay(s, identifier);
99
+ if (!relay) return null;
100
+ const cleanLabel = (label || "").trim();
101
+ relay.label = cleanLabel || undefined;
102
+ return relay;
103
+ }
104
+
105
+ /**
106
+ * Find a relay in state by 1-based index, short name / label, or URL.
107
+ */
108
+ export function findRelay(
109
+ s: RelayState,
110
+ identifier: string | number,
111
+ ): KnownRelay | undefined {
112
+ if (typeof identifier === "number") {
113
+ const idx = identifier - 1;
114
+ return s.relays[idx];
59
115
  }
60
- s.relays.push({ url, label, addedAt: new Date().toISOString() });
116
+ const str = String(identifier || "").trim();
117
+ if (!str) return undefined;
118
+
119
+ // 1-based index (e.g. "1", "2")
120
+ if (/^\d+$/.test(str)) {
121
+ const num = Number.parseInt(str, 10);
122
+ if (num >= 1 && num <= s.relays.length) {
123
+ return s.relays[num - 1];
124
+ }
125
+ }
126
+
127
+ // Exact URL match
128
+ const exactUrl = s.relays.find((r) => r.url === str);
129
+ if (exactUrl) return exactUrl;
130
+
131
+ // Case-insensitive label match
132
+ const byLabel = s.relays.find(
133
+ (r) => r.label && r.label.toLowerCase() === str.toLowerCase(),
134
+ );
135
+ if (byLabel) return byLabel;
136
+
137
+ // Partial URL match
138
+ return s.relays.find((r) => r.url.toLowerCase().includes(str.toLowerCase()));
61
139
  }
62
140
 
63
141
  /**
@@ -87,14 +165,13 @@ export function resolveRelayState(): RelayState {
87
165
  return s;
88
166
  }
89
167
 
90
- // In-memory global relay state
91
168
  let activeRelayState: RelayState = resolveRelayState();
92
169
  // Monotonic counter to distribute primary relay across concurrent subagents
93
170
  let roundRobinCounter = 0;
94
171
  let activeStatusUi: ExtensionUIContext | null = null;
172
+ let isFreeFlowModelActive = true;
95
173
  /**
96
174
  * Mtime of the on-disk state file at the moment we last read or wrote it.
97
- * Lets worker processes pick up relay-pool changes persisted by another
98
175
  * session's master daemon, while never clobbering this process's own
99
176
  * unpersisted runtime overrides between external writes.
100
177
  */
@@ -140,6 +217,12 @@ export function setStatusUi(ui: ExtensionUIContext | null): void {
140
217
  activeStatusUi = ui;
141
218
  }
142
219
 
220
+ export function setFreeFlowModelActive(active: boolean): void {
221
+ isFreeFlowModelActive = active;
222
+ if (!active && activeStatusUi?.setStatus) {
223
+ activeStatusUi.setStatus("freeflow", undefined);
224
+ }
225
+ }
143
226
  /**
144
227
  * Get the current Pi extension UI context
145
228
  */
@@ -149,13 +232,27 @@ export function getStatusUi(): ExtensionUIContext | null {
149
232
 
150
233
  /**
151
234
  * Generate a short, human-readable label for a relay URL.
235
+ * Prefers user-configured label/short name; falls back to clean domain/subdomain.
152
236
  */
153
237
  export function shortRelayLabel(url: string, relays?: KnownRelay[]): string {
154
238
  const pool = relays || activeRelayState.relays;
155
239
  try {
156
240
  const hit = pool.find((r) => r.url === url);
157
- if (hit?.label) return hit.label;
158
- return new URL(url).host.split(".")[0];
241
+ if (hit?.label?.trim() && hit.label.trim() !== "manual") {
242
+ return hit.label.trim();
243
+ }
244
+ const u = new URL(url);
245
+ const host = u.host;
246
+ // IP address (e.g. 192.168.1.5:8080 or 10.0.0.1)
247
+ if (/^(\d{1,3}\.){3}\d{1,3}(:\d+)?$/.test(host)) {
248
+ return host;
249
+ }
250
+ const parts = host.split(".");
251
+ if (parts.length >= 3) {
252
+ // E.g. "my-relay" from "my-relay.workers.dev" or "my-app.vercel.app"
253
+ return parts[0];
254
+ }
255
+ return parts[0] || host;
159
256
  } catch {
160
257
  return url.slice(0, 18);
161
258
  }
@@ -206,13 +303,20 @@ export function updateRelayStatusUi(targetUrl?: string): void {
206
303
  if (!activeStatusUi?.setStatus) {
207
304
  return;
208
305
  }
209
- const currentUrl = targetUrl || activeRelayState.url;
210
- if (!activeRelayState.enabled || !currentUrl) {
211
- activeStatusUi.setStatus("freeflow", activeRelayState.enabled ? "relay: direct" : "relay: OFF");
306
+ // Do not update status bar if the user switched to another provider (e.g. Gemini/Claude)
307
+ if (!isFreeFlowModelActive) {
308
+ activeStatusUi.setStatus("freeflow", undefined);
309
+ return;
310
+ }
311
+ const state = getActiveRelayState();
312
+ if (!state.enabled || !state.relays || state.relays.length === 0) {
313
+ activeStatusUi.setStatus("freeflow", undefined);
212
314
  return;
213
315
  }
316
+ const currentUrl = targetUrl || state.url || state.relays[0]?.url || "";
214
317
  const label = shortRelayLabel(currentUrl);
215
- const total = activeRelayState.relays.length || 1;
216
- const pos = Math.max(1, activeRelayState.relays.findIndex((r) => r.url === currentUrl) + 1);
217
- activeStatusUi.setStatus("freeflow", `relay: ON | ${label} ${pos}/${total}`);
318
+ const total = state.relays.length || 1;
319
+ const pos = Math.max(1, state.relays.findIndex((r) => r.url === currentUrl) + 1);
320
+ const modeLabel = state.mode === "on" ? "ON" : "AUTO (ON)";
321
+ activeStatusUi.setStatus("freeflow", `relay: ${modeLabel} | ${label} ${pos}/${total}`);
218
322
  }
package/src/types.ts CHANGED
@@ -39,13 +39,14 @@ export interface KnownRelay {
39
39
  label?: string;
40
40
  addedAt?: string;
41
41
  }
42
+ export type RelayMode = "auto" | "on" | "off";
42
43
 
43
44
  export interface RelayState {
45
+ mode?: RelayMode;
44
46
  enabled: boolean;
45
47
  url: string;
46
48
  relays: KnownRelay[];
47
49
  }
48
-
49
50
  export type LogLevel = "debug" | "info" | "warn" | "error" | "audit";
50
51
 
51
52
  export interface DebugState {