pi-free 2.0.9 → 2.0.10

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/config.ts CHANGED
@@ -1,337 +1,349 @@
1
- /**
2
- * Shared config for pi-free-providers.
3
- *
4
- * Keys and flags are resolved in this order (first wins):
5
- * 1. Environment variable
6
- * 2. ~/.pi/free.json
7
- *
8
- * All exported values are getter functions so that runtime changes
9
- * (e.g. after toggle-{provider}) are visible immediately.
10
- */
11
-
12
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
- import { join } from "node:path";
14
- export {
15
- PROVIDER_CLINE,
16
- PROVIDER_KILO,
17
- PROVIDER_MODAL,
18
- PROVIDER_NVIDIA,
19
- PROVIDER_QWEN,
20
- } from "./constants.ts";
21
- import { createLogger } from "./lib/logger.ts";
22
-
23
- const _logger = createLogger("config");
24
-
25
- interface PiFreeConfig {
26
- nvidia_api_key?: string;
27
- ollama_api_key?: string;
28
- zenmux_api_key?: string;
29
- crofai_api_key?: string;
30
- codestral_api_key?: string;
31
- mistral_api_key?: string;
32
- llm7_api_key?: string;
33
- deepinfra_api_key?: string;
34
- sambanova_api_key?: string;
35
- groq_api_key?: string;
36
- cerebras_api_key?: string;
37
- xai_api_key?: string;
38
- hf_token?: string;
39
- kilo_free_only?: boolean;
40
- hidden_models?: string[];
41
- free_only?: boolean;
42
- kilo_show_paid?: boolean;
43
- ollama_show_paid?: boolean;
44
- cline_show_paid?: boolean;
45
- zenmux_show_paid?: boolean;
46
- crofai_show_paid?: boolean;
47
- codestral_show_paid?: boolean;
48
- llm7_show_paid?: boolean;
49
- deepinfra_show_paid?: boolean;
50
- sambanova_show_paid?: boolean;
51
- openrouter_show_paid?: boolean;
52
- opencode_show_paid?: boolean;
53
- }
54
-
55
- const CONFIG_TEMPLATE: PiFreeConfig = {
56
- nvidia_api_key: "",
57
- ollama_api_key: "",
58
- zenmux_api_key: "",
59
- crofai_api_key: "",
60
- codestral_api_key: "",
61
- mistral_api_key: "",
62
- llm7_api_key: "",
63
- deepinfra_api_key: "",
64
- sambanova_api_key: "",
65
- groq_api_key: "",
66
- cerebras_api_key: "",
67
- xai_api_key: "",
68
- hf_token: "",
69
-
70
- kilo_free_only: false,
71
- hidden_models: [],
72
- free_only: true,
73
- kilo_show_paid: false,
74
- ollama_show_paid: false,
75
- cline_show_paid: false,
76
- zenmux_show_paid: false,
77
- crofai_show_paid: false,
78
- codestral_show_paid: false,
79
- llm7_show_paid: false,
80
- deepinfra_show_paid: false,
81
- sambanova_show_paid: false,
82
- openrouter_show_paid: false,
83
- opencode_show_paid: false,
84
- };
85
-
86
- const PI_DIR = join(process.env.HOME || process.env.USERPROFILE || "", ".pi");
87
- const CONFIG_PATH = join(PI_DIR, "free.json");
88
-
89
- function ensureConfigFile(): void {
90
- try {
91
- mkdirSync(PI_DIR, { recursive: true });
92
- if (existsSync(CONFIG_PATH)) {
93
- const existing = JSON.parse(
94
- readFileSync(CONFIG_PATH, "utf8"),
95
- ) as PiFreeConfig;
96
- const merged = { ...CONFIG_TEMPLATE, ...existing };
97
- if (JSON.stringify(merged) !== JSON.stringify(existing)) {
98
- writeFileSync(
99
- CONFIG_PATH,
100
- `${JSON.stringify(merged, null, 2)}\n`,
101
- "utf8",
102
- );
103
- }
104
- } else {
105
- writeFileSync(
106
- CONFIG_PATH,
107
- `${JSON.stringify(CONFIG_TEMPLATE, null, 2)}\n`,
108
- "utf8",
109
- );
110
- }
111
- } catch (err) {
112
- _logger.warn("Could not create config file", {
113
- path: CONFIG_PATH,
114
- error: err instanceof Error ? err.message : String(err),
115
- });
116
- }
117
- }
118
-
119
- export function loadConfigFile(): PiFreeConfig {
120
- try {
121
- return JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as PiFreeConfig;
122
- } catch (err) {
123
- _logger.warn("Could not parse config file — returning empty config", {
124
- path: CONFIG_PATH,
125
- error: err instanceof Error ? err.message : String(err),
126
- });
127
- return {};
128
- }
129
- }
130
-
131
- ensureConfigFile();
132
-
133
- // Resolve each value: env var takes priority over config file.
134
- function resolve(envKey: string, fileVal?: string): string | undefined {
135
- return process.env[envKey] || (fileVal?.trim() ? fileVal : undefined);
136
- }
137
-
138
- // Resolve boolean flag: env var takes priority, then config file.
139
- function resolveBool(envKey: string, fileVal?: boolean): boolean {
140
- const envValue = process.env[envKey];
141
- if (envValue === "true") return true;
142
- if (envValue === "false") return false;
143
- return fileVal === true;
144
- }
145
-
146
- // =============================================================================
147
- // Per-provider paid-model flags (getters so toggles reflect immediately)
148
- // =============================================================================
149
-
150
- export function getKiloShowPaid(): boolean {
151
- return resolveBool("KILO_SHOW_PAID", loadConfigFile().kilo_show_paid);
152
- }
153
-
154
- export function getClineShowPaid(): boolean {
155
- return resolveBool("CLINE_SHOW_PAID", loadConfigFile().cline_show_paid);
156
- }
157
-
158
- export function getZenmuxShowPaid(): boolean {
159
- return resolveBool("ZENMUX_SHOW_PAID", loadConfigFile().zenmux_show_paid);
160
- }
161
-
162
- export function getCrofaiShowPaid(): boolean {
163
- return resolveBool("CROFAI_SHOW_PAID", loadConfigFile().crofai_show_paid);
164
- }
165
-
166
- export function getCodestralShowPaid(): boolean {
167
- return resolveBool(
168
- "CODESTRAL_SHOW_PAID",
169
- loadConfigFile().codestral_show_paid,
170
- );
171
- }
172
-
173
- export function getLlm7ShowPaid(): boolean {
174
- return resolveBool("LLM7_SHOW_PAID", loadConfigFile().llm7_show_paid);
175
- }
176
-
177
- export function getDeepinfraShowPaid(): boolean {
178
- return resolveBool(
179
- "DEEPINFRA_SHOW_PAID",
180
- loadConfigFile().deepinfra_show_paid,
181
- );
182
- }
183
-
184
- export function getSambanovaShowPaid(): boolean {
185
- return resolveBool(
186
- "SAMBANOVA_SHOW_PAID",
187
- loadConfigFile().sambanova_show_paid,
188
- );
189
- }
190
-
191
- export function getOllamaShowPaid(): boolean {
192
- return resolveBool("OLLAMA_SHOW_PAID", loadConfigFile().ollama_show_paid);
193
- }
194
-
195
- export function getOpenrouterShowPaid(): boolean {
196
- return resolveBool(
197
- "OPENROUTER_SHOW_PAID",
198
- loadConfigFile().openrouter_show_paid,
199
- );
200
- }
201
-
202
- export function getOpencodeShowPaid(): boolean {
203
- return resolveBool("OPENCODE_SHOW_PAID", loadConfigFile().opencode_show_paid);
204
- }
205
-
206
- // =============================================================================
207
- // Global free-only mode
208
- // =============================================================================
209
-
210
- export function getFreeOnly(): boolean {
211
- return resolveBool("PI_FREE_ONLY", loadConfigFile().free_only);
212
- }
213
-
214
- export function getKiloFreeOnly(): boolean {
215
- return resolveBool("PI_FREE_KILO_FREE_ONLY", loadConfigFile().kilo_free_only);
216
- }
217
-
218
- // =============================================================================
219
- // API Keys (getters so runtime config changes are visible)
220
- // =============================================================================
221
-
222
- export function getNvidiaApiKey(): string | undefined {
223
- return resolve("NVIDIA_API_KEY", loadConfigFile().nvidia_api_key);
224
- }
225
-
226
- export function getZenmuxApiKey(): string | undefined {
227
- return resolve("ZENMUX_API_KEY", loadConfigFile().zenmux_api_key);
228
- }
229
-
230
- export function getCrofaiApiKey(): string | undefined {
231
- return resolve("CROFAI_API_KEY", loadConfigFile().crofai_api_key);
232
- }
233
-
234
- export function getCodestralApiKey(): string | undefined {
235
- return resolve("CODESTRAL_API_KEY", loadConfigFile().codestral_api_key);
236
- }
237
-
238
- export function getLlm7ApiKey(): string | undefined {
239
- return resolve("LLM7_API_KEY", loadConfigFile().llm7_api_key);
240
- }
241
-
242
- export function getDeepinfraApiKey(): string | undefined {
243
- return resolve("DEEPINFRA_TOKEN", loadConfigFile().deepinfra_api_key);
244
- }
245
-
246
- export function getSambanovaApiKey(): string | undefined {
247
- return resolve("SAMBANOVA_API_KEY", loadConfigFile().sambanova_api_key);
248
- }
249
-
250
- export function getOllamaApiKey(): string | undefined {
251
- return resolve("OLLAMA_API_KEY", loadConfigFile().ollama_api_key);
252
- }
253
-
254
- export function getMistralApiKey(): string | undefined {
255
- return resolve("MISTRAL_API_KEY", loadConfigFile().mistral_api_key);
256
- }
257
-
258
- export function getGroqApiKey(): string | undefined {
259
- return resolve("GROQ_API_KEY", loadConfigFile().groq_api_key);
260
- }
261
-
262
- export function getCerebrasApiKey(): string | undefined {
263
- return resolve("CEREBRAS_API_KEY", loadConfigFile().cerebras_api_key);
264
- }
265
-
266
- export function getXaiApiKey(): string | undefined {
267
- return resolve("XAI_API_KEY", loadConfigFile().xai_api_key);
268
- }
269
-
270
- export function getHfToken(): string | undefined {
271
- return resolve("HF_TOKEN", loadConfigFile().hf_token);
272
- }
273
-
274
- /**
275
- * OpenRouter key — pi's built-in provider reads from ~/.pi/agent/auth.json.
276
- * pi-free only checks the env var to avoid stale keys from free.json.
277
- */
278
- export function getOpenrouterApiKey(): string | undefined {
279
- return process.env.OPENROUTER_API_KEY;
280
- }
281
-
282
- // =============================================================================
283
- // Hidden models (re-reads config on every call)
284
- // =============================================================================
285
-
286
- /**
287
- * Apply hidden models filter with provider scoping.
288
- * Hidden models can be specified as:
289
- * - "model-id" (global, applies to all providers - deprecated)
290
- * - "provider/model-id" (provider-specific, preferred)
291
- */
292
- export function applyHidden<T extends { id: string }>(
293
- models: T[],
294
- providerId?: string,
295
- ): T[] {
296
- const hidden = new Set(loadConfigFile().hidden_models ?? []);
297
- if (hidden.size === 0) return models;
298
-
299
- return models.filter((m) => {
300
- // Check provider-scoped ID (preferred format: "provider/model-id")
301
- if (providerId && hidden.has(`${providerId}/${m.id}`)) {
302
- return false;
303
- }
304
- // Check global ID (legacy format, still supported for backward compat)
305
- if (hidden.has(m.id)) {
306
- return false;
307
- }
308
- return true;
309
- });
310
- }
311
-
312
- // =============================================================================
313
- // Persistence
314
- // =============================================================================
315
-
316
- export function saveConfig(updates: Partial<PiFreeConfig>): void {
317
- try {
318
- const existing = loadConfigFile();
319
- const merged = { ...existing, ...updates };
320
- writeFileSync(CONFIG_PATH, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
321
- _logger.info("Config saved", {
322
- path: CONFIG_PATH,
323
- keys: Object.keys(updates),
324
- });
325
- } catch (err) {
326
- _logger.error("Failed to save config", {
327
- path: CONFIG_PATH,
328
- error: err instanceof Error ? err.message : String(err),
329
- });
330
- }
331
- }
332
-
333
- export function getConfig(): PiFreeConfig {
334
- return loadConfigFile();
335
- }
336
-
337
- // =============================================================================
1
+ /**
2
+ * Shared config for pi-free-providers.
3
+ *
4
+ * Keys and flags are resolved in this order (first wins):
5
+ * 1. Environment variable
6
+ * 2. ~/.pi/free.json
7
+ *
8
+ * All exported values are getter functions so that runtime changes
9
+ * (e.g. after toggle-{provider}) are visible immediately.
10
+ */
11
+
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ export {
15
+ PROVIDER_CLINE,
16
+ PROVIDER_KILO,
17
+ PROVIDER_MODAL,
18
+ PROVIDER_NVIDIA,
19
+ PROVIDER_QWEN,
20
+ } from "./constants.ts";
21
+ import { createLogger } from "./lib/logger.ts";
22
+
23
+ const _logger = createLogger("config");
24
+
25
+ interface PiFreeConfig {
26
+ nvidia_api_key?: string;
27
+ ollama_api_key?: string;
28
+ zenmux_api_key?: string;
29
+ crofai_api_key?: string;
30
+ codestral_api_key?: string;
31
+ mistral_api_key?: string;
32
+ llm7_api_key?: string;
33
+ deepinfra_api_key?: string;
34
+ sambanova_api_key?: string;
35
+ together_api_key?: string;
36
+ groq_api_key?: string;
37
+ cerebras_api_key?: string;
38
+ xai_api_key?: string;
39
+ hf_token?: string;
40
+ kilo_free_only?: boolean;
41
+ hidden_models?: string[];
42
+ free_only?: boolean;
43
+ kilo_show_paid?: boolean;
44
+ ollama_show_paid?: boolean;
45
+ cline_show_paid?: boolean;
46
+ zenmux_show_paid?: boolean;
47
+ crofai_show_paid?: boolean;
48
+ codestral_show_paid?: boolean;
49
+ llm7_show_paid?: boolean;
50
+ deepinfra_show_paid?: boolean;
51
+ sambanova_show_paid?: boolean;
52
+ together_show_paid?: boolean;
53
+ openrouter_show_paid?: boolean;
54
+ opencode_show_paid?: boolean;
55
+ }
56
+
57
+ const CONFIG_TEMPLATE: PiFreeConfig = {
58
+ nvidia_api_key: "",
59
+ ollama_api_key: "",
60
+ zenmux_api_key: "",
61
+ crofai_api_key: "",
62
+ codestral_api_key: "",
63
+ mistral_api_key: "",
64
+ llm7_api_key: "",
65
+ deepinfra_api_key: "",
66
+ sambanova_api_key: "",
67
+ together_api_key: "",
68
+ groq_api_key: "",
69
+ cerebras_api_key: "",
70
+ xai_api_key: "",
71
+ hf_token: "",
72
+
73
+ kilo_free_only: false,
74
+ hidden_models: [],
75
+ free_only: true,
76
+ kilo_show_paid: false,
77
+ ollama_show_paid: false,
78
+ cline_show_paid: false,
79
+ zenmux_show_paid: false,
80
+ crofai_show_paid: false,
81
+ codestral_show_paid: false,
82
+ llm7_show_paid: false,
83
+ deepinfra_show_paid: false,
84
+ sambanova_show_paid: false,
85
+ together_show_paid: false,
86
+ openrouter_show_paid: false,
87
+ opencode_show_paid: false,
88
+ };
89
+
90
+ const PI_DIR = join(process.env.HOME || process.env.USERPROFILE || "", ".pi");
91
+ const CONFIG_PATH = join(PI_DIR, "free.json");
92
+
93
+ function ensureConfigFile(): void {
94
+ try {
95
+ mkdirSync(PI_DIR, { recursive: true });
96
+ if (existsSync(CONFIG_PATH)) {
97
+ const existing = JSON.parse(
98
+ readFileSync(CONFIG_PATH, "utf8"),
99
+ ) as PiFreeConfig;
100
+ const merged = { ...CONFIG_TEMPLATE, ...existing };
101
+ if (JSON.stringify(merged) !== JSON.stringify(existing)) {
102
+ writeFileSync(
103
+ CONFIG_PATH,
104
+ `${JSON.stringify(merged, null, 2)}\n`,
105
+ "utf8",
106
+ );
107
+ }
108
+ } else {
109
+ writeFileSync(
110
+ CONFIG_PATH,
111
+ `${JSON.stringify(CONFIG_TEMPLATE, null, 2)}\n`,
112
+ "utf8",
113
+ );
114
+ }
115
+ } catch (err) {
116
+ _logger.warn("Could not create config file", {
117
+ path: CONFIG_PATH,
118
+ error: err instanceof Error ? err.message : String(err),
119
+ });
120
+ }
121
+ }
122
+
123
+ export function loadConfigFile(): PiFreeConfig {
124
+ try {
125
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as PiFreeConfig;
126
+ } catch (err) {
127
+ _logger.warn("Could not parse config file — returning empty config", {
128
+ path: CONFIG_PATH,
129
+ error: err instanceof Error ? err.message : String(err),
130
+ });
131
+ return {};
132
+ }
133
+ }
134
+
135
+ ensureConfigFile();
136
+
137
+ // Resolve each value: env var takes priority over config file.
138
+ function resolve(envKey: string, fileVal?: string): string | undefined {
139
+ return process.env[envKey] || (fileVal?.trim() ? fileVal : undefined);
140
+ }
141
+
142
+ // Resolve boolean flag: env var takes priority, then config file.
143
+ function resolveBool(envKey: string, fileVal?: boolean): boolean {
144
+ const envValue = process.env[envKey];
145
+ if (envValue === "true") return true;
146
+ if (envValue === "false") return false;
147
+ return fileVal === true;
148
+ }
149
+
150
+ // =============================================================================
151
+ // Per-provider paid-model flags (getters so toggles reflect immediately)
152
+ // =============================================================================
153
+
154
+ export function getKiloShowPaid(): boolean {
155
+ return resolveBool("KILO_SHOW_PAID", loadConfigFile().kilo_show_paid);
156
+ }
157
+
158
+ export function getClineShowPaid(): boolean {
159
+ return resolveBool("CLINE_SHOW_PAID", loadConfigFile().cline_show_paid);
160
+ }
161
+
162
+ export function getZenmuxShowPaid(): boolean {
163
+ return resolveBool("ZENMUX_SHOW_PAID", loadConfigFile().zenmux_show_paid);
164
+ }
165
+
166
+ export function getCrofaiShowPaid(): boolean {
167
+ return resolveBool("CROFAI_SHOW_PAID", loadConfigFile().crofai_show_paid);
168
+ }
169
+
170
+ export function getCodestralShowPaid(): boolean {
171
+ return resolveBool(
172
+ "CODESTRAL_SHOW_PAID",
173
+ loadConfigFile().codestral_show_paid,
174
+ );
175
+ }
176
+
177
+ export function getLlm7ShowPaid(): boolean {
178
+ return resolveBool("LLM7_SHOW_PAID", loadConfigFile().llm7_show_paid);
179
+ }
180
+
181
+ export function getDeepinfraShowPaid(): boolean {
182
+ return resolveBool(
183
+ "DEEPINFRA_SHOW_PAID",
184
+ loadConfigFile().deepinfra_show_paid,
185
+ );
186
+ }
187
+
188
+ export function getSambanovaShowPaid(): boolean {
189
+ return resolveBool(
190
+ "SAMBANOVA_SHOW_PAID",
191
+ loadConfigFile().sambanova_show_paid,
192
+ );
193
+ }
194
+
195
+ export function getTogetherShowPaid(): boolean {
196
+ return resolveBool("TOGETHER_SHOW_PAID", loadConfigFile().together_show_paid);
197
+ }
198
+
199
+ export function getOllamaShowPaid(): boolean {
200
+ return resolveBool("OLLAMA_SHOW_PAID", loadConfigFile().ollama_show_paid);
201
+ }
202
+
203
+ export function getOpenrouterShowPaid(): boolean {
204
+ return resolveBool(
205
+ "OPENROUTER_SHOW_PAID",
206
+ loadConfigFile().openrouter_show_paid,
207
+ );
208
+ }
209
+
210
+ export function getOpencodeShowPaid(): boolean {
211
+ return resolveBool("OPENCODE_SHOW_PAID", loadConfigFile().opencode_show_paid);
212
+ }
213
+
214
+ // =============================================================================
215
+ // Global free-only mode
216
+ // =============================================================================
217
+
218
+ export function getFreeOnly(): boolean {
219
+ return resolveBool("PI_FREE_ONLY", loadConfigFile().free_only);
220
+ }
221
+
222
+ export function getKiloFreeOnly(): boolean {
223
+ return resolveBool("PI_FREE_KILO_FREE_ONLY", loadConfigFile().kilo_free_only);
224
+ }
225
+
226
+ // =============================================================================
227
+ // API Keys (getters so runtime config changes are visible)
228
+ // =============================================================================
229
+
230
+ export function getNvidiaApiKey(): string | undefined {
231
+ return resolve("NVIDIA_API_KEY", loadConfigFile().nvidia_api_key);
232
+ }
233
+
234
+ export function getZenmuxApiKey(): string | undefined {
235
+ return resolve("ZENMUX_API_KEY", loadConfigFile().zenmux_api_key);
236
+ }
237
+
238
+ export function getCrofaiApiKey(): string | undefined {
239
+ return resolve("CROFAI_API_KEY", loadConfigFile().crofai_api_key);
240
+ }
241
+
242
+ export function getCodestralApiKey(): string | undefined {
243
+ return resolve("CODESTRAL_API_KEY", loadConfigFile().codestral_api_key);
244
+ }
245
+
246
+ export function getLlm7ApiKey(): string | undefined {
247
+ return resolve("LLM7_API_KEY", loadConfigFile().llm7_api_key);
248
+ }
249
+
250
+ export function getDeepinfraApiKey(): string | undefined {
251
+ return resolve("DEEPINFRA_TOKEN", loadConfigFile().deepinfra_api_key);
252
+ }
253
+
254
+ export function getSambanovaApiKey(): string | undefined {
255
+ return resolve("SAMBANOVA_API_KEY", loadConfigFile().sambanova_api_key);
256
+ }
257
+
258
+ export function getTogetherApiKey(): string | undefined {
259
+ return resolve("TOGETHER_AI_API_KEY", loadConfigFile().together_api_key);
260
+ }
261
+
262
+ export function getOllamaApiKey(): string | undefined {
263
+ return resolve("OLLAMA_API_KEY", loadConfigFile().ollama_api_key);
264
+ }
265
+
266
+ export function getMistralApiKey(): string | undefined {
267
+ return resolve("MISTRAL_API_KEY", loadConfigFile().mistral_api_key);
268
+ }
269
+
270
+ export function getGroqApiKey(): string | undefined {
271
+ return resolve("GROQ_API_KEY", loadConfigFile().groq_api_key);
272
+ }
273
+
274
+ export function getCerebrasApiKey(): string | undefined {
275
+ return resolve("CEREBRAS_API_KEY", loadConfigFile().cerebras_api_key);
276
+ }
277
+
278
+ export function getXaiApiKey(): string | undefined {
279
+ return resolve("XAI_API_KEY", loadConfigFile().xai_api_key);
280
+ }
281
+
282
+ export function getHfToken(): string | undefined {
283
+ return resolve("HF_TOKEN", loadConfigFile().hf_token);
284
+ }
285
+
286
+ /**
287
+ * OpenRouter key pi's built-in provider reads from ~/.pi/agent/auth.json.
288
+ * pi-free only checks the env var to avoid stale keys from free.json.
289
+ */
290
+ export function getOpenrouterApiKey(): string | undefined {
291
+ return process.env.OPENROUTER_API_KEY;
292
+ }
293
+
294
+ // =============================================================================
295
+ // Hidden models (re-reads config on every call)
296
+ // =============================================================================
297
+
298
+ /**
299
+ * Apply hidden models filter with provider scoping.
300
+ * Hidden models can be specified as:
301
+ * - "model-id" (global, applies to all providers - deprecated)
302
+ * - "provider/model-id" (provider-specific, preferred)
303
+ */
304
+ export function applyHidden<T extends { id: string }>(
305
+ models: T[],
306
+ providerId?: string,
307
+ ): T[] {
308
+ const hidden = new Set(loadConfigFile().hidden_models ?? []);
309
+ if (hidden.size === 0) return models;
310
+
311
+ return models.filter((m) => {
312
+ // Check provider-scoped ID (preferred format: "provider/model-id")
313
+ if (providerId && hidden.has(`${providerId}/${m.id}`)) {
314
+ return false;
315
+ }
316
+ // Check global ID (legacy format, still supported for backward compat)
317
+ if (hidden.has(m.id)) {
318
+ return false;
319
+ }
320
+ return true;
321
+ });
322
+ }
323
+
324
+ // =============================================================================
325
+ // Persistence
326
+ // =============================================================================
327
+
328
+ export function saveConfig(updates: Partial<PiFreeConfig>): void {
329
+ try {
330
+ const existing = loadConfigFile();
331
+ const merged = { ...existing, ...updates };
332
+ writeFileSync(CONFIG_PATH, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
333
+ _logger.info("Config saved", {
334
+ path: CONFIG_PATH,
335
+ keys: Object.keys(updates),
336
+ });
337
+ } catch (err) {
338
+ _logger.error("Failed to save config", {
339
+ path: CONFIG_PATH,
340
+ error: err instanceof Error ? err.message : String(err),
341
+ });
342
+ }
343
+ }
344
+
345
+ export function getConfig(): PiFreeConfig {
346
+ return loadConfigFile();
347
+ }
348
+
349
+ // =============================================================================