@evandu/super-pi 0.1.120 → 0.1.121

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.
@@ -30,5 +30,5 @@
30
30
  "src/client/types.ts": "7654e95b65053d639634e0bbee2b4524831571ec59c79d4b8384d1eeb662b0cf",
31
31
  "src/client/vite-env.d.ts": "65996936fbb042915f7b74a200fcdde7e410f32a669b1ab9597cfaa4b0faddb5"
32
32
  },
33
- "recordedAt": "2026-10-23T06:23:16.694Z"
33
+ "recordedAt": "2026-10-23T06:47:25.078Z"
34
34
  }
@@ -49,7 +49,7 @@ export declare class ProviderService {
49
49
  * asks for it to go.
50
50
  */
51
51
  set(input: unknown): Promise<PublicProvider[]>;
52
- /** Removes a provider Super Pi manages, and only one it manages. */
52
+ /** Removes a provider from Pi's models.json and auth.json. */
53
53
  remove(providerId: string): Promise<PublicProvider[]>;
54
54
  /**
55
55
  * Removes ONE model from a provider, leaving the provider and its other models alone.
@@ -101,7 +101,7 @@ export class ProviderService {
101
101
  await this.afterChange();
102
102
  return publicProviders(next);
103
103
  }
104
- /** Removes a provider Super Pi manages, and only one it manages. */
104
+ /** Removes a provider from Pi's models.json and auth.json. */
105
105
  async remove(providerId) {
106
106
  const previous = await this.registry.load();
107
107
  const existing = previous.find((provider) => provider.id === providerId);
@@ -62,6 +62,7 @@ export type ProviderDefinition = {
62
62
  api: string;
63
63
  /** The API key. Stored, never broadcast. */
64
64
  apiKey?: string;
65
+ hasAuthKey?: boolean;
65
66
  models: ProviderModelDefinition[];
66
67
  };
67
68
  /** One model, as the browser sees it. No key, no nothing secret - there is nothing secret here. */
@@ -116,36 +117,19 @@ export declare class ProviderRegistry {
116
117
  /** Super Pi's own registry: the providers it manages, with their keys. */
117
118
  private get registryPath();
118
119
  /**
119
- * Reads the registry.
120
- *
121
- * A missing file is an empty registry, not an error: nobody has added a provider yet. A file that
122
- * cannot be parsed IS an error, because reading it as empty would let the next write replace
123
- * somebody's keys with nothing.
120
+ * Reads all configured providers directly from Pi's models.json and auth.json.
121
+ * Super Pi acts as the direct visual management plane for Pi's model configurations.
124
122
  */
125
123
  load(): Promise<ProviderDefinition[]>;
126
124
  /** Writes the registry. The key lives here, so the file is 0600. */
127
125
  save(providers: ProviderDefinition[]): Promise<void>;
128
126
  /**
129
- * Makes Pi's files match the registry.
130
- *
131
- * `previousIds` are the providers Super Pi managed before this change: they are the only ones
132
- * whose entries may be REMOVED. An id that is in Pi's `models.json` and not in `previousIds` belongs
133
- * to the person who wrote that file, and touching it would be Super Pi editing a provider it does
134
- * not own - so the write is refused with a message naming the id.
127
+ * Directly updates Pi's models.json and auth.json to match the new set of providers.
128
+ * Full visual editor semantics: any provider in models.json is editable and removable.
135
129
  */
136
130
  applyToPi(previous: ProviderDefinition[], next: ProviderDefinition[]): Promise<void>;
137
131
  /**
138
132
  * Writes the keys into Pi's `auth.json`.
139
- *
140
- * A credential is REMOVED only when it is the one Super Pi wrote: the same provider id and the
141
- * same key. A person who replaced our key with their own - or who ran Pi's `/login` and put an
142
- * OAuth token there - keeps it. Deleting somebody else's credential because they removed our
143
- * provider would be the worst version of this feature.
144
- *
145
- * TWO CASES, and the second is the one that is easy to miss: the provider can be gone ENTIRELY
146
- * (it was removed), or it can still be there with its key GONE (the person asked for the key to be
147
- * cleared). Both have to take the credential with them, or "clear the key" would leave a working
148
- * key sitting in Pi's auth file.
149
133
  */
150
134
  private applyKeysToPi;
151
135
  }
@@ -224,32 +224,65 @@ export class ProviderRegistry {
224
224
  return join(this.dataDir, "providers.json");
225
225
  }
226
226
  /**
227
- * Reads the registry.
228
- *
229
- * A missing file is an empty registry, not an error: nobody has added a provider yet. A file that
230
- * cannot be parsed IS an error, because reading it as empty would let the next write replace
231
- * somebody's keys with nothing.
227
+ * Reads all configured providers directly from Pi's models.json and auth.json.
228
+ * Super Pi acts as the direct visual management plane for Pi's model configurations.
232
229
  */
233
230
  async load() {
234
- const raw = await readJsonFile(this.registryPath);
235
- if (!raw || typeof raw !== "object")
236
- return [];
237
- const providers = raw.providers;
238
- if (!Array.isArray(providers))
239
- return [];
240
- return providers.filter((provider) => Boolean(provider) && typeof provider === "object");
231
+ const modelsPath = piModelsPath(this.piDir);
232
+ const authPath = piAuthPath(this.piDir);
233
+ const piModels = (await readJsonFile(modelsPath))?.providers ?? {};
234
+ const piAuth = (await readJsonFile(authPath)) ?? {};
235
+ // Backup registry from Super Pi data dir if needed
236
+ const rawBackup = await readJsonFile(this.registryPath);
237
+ const backupList = rawBackup && typeof rawBackup === "object" && Array.isArray(rawBackup.providers)
238
+ ? rawBackup.providers.filter((p) => Boolean(p) && typeof p === "object")
239
+ : [];
240
+ const backupMap = new Map(backupList.map((p) => [p.id, p]));
241
+ const providers = [];
242
+ const seen = new Set();
243
+ for (const [pId, pData] of Object.entries(piModels)) {
244
+ if (!pData || typeof pData !== "object")
245
+ continue;
246
+ seen.add(pId);
247
+ const rec = pData;
248
+ const backup = backupMap.get(pId);
249
+ const baseUrl = typeof rec.baseUrl === "string" ? rec.baseUrl : (backup?.baseUrl ?? "");
250
+ const api = typeof rec.api === "string" ? rec.api : (backup?.api ?? "openai-completions");
251
+ const name = typeof rec.name === "string" ? rec.name : (backup?.name ?? pId);
252
+ const rawModels = Array.isArray(rec.models) ? rec.models : (backup?.models ?? []);
253
+ const models = rawModels.map((m) => ({
254
+ id: typeof m?.id === "string" ? m.id : String(m),
255
+ name: typeof m?.name === "string" ? m.name : m?.id,
256
+ reasoning: Boolean(m?.reasoning),
257
+ imageInput: Array.isArray(m?.input) ? m.input.includes("image") : false,
258
+ contextWindow: typeof m?.contextWindow === "number" ? m.contextWindow : undefined,
259
+ maxTokens: typeof m?.maxTokens === "number" ? m.maxTokens : undefined,
260
+ }));
261
+ const apiKey = backup?.apiKey;
262
+ providers.push({
263
+ id: pId,
264
+ name,
265
+ baseUrl,
266
+ api,
267
+ ...(apiKey ? { apiKey } : {}),
268
+ hasAuthKey: Boolean(piAuth[pId]?.key || backup?.apiKey),
269
+ models,
270
+ });
271
+ }
272
+ for (const b of backupList) {
273
+ if (!seen.has(b.id)) {
274
+ providers.push(b);
275
+ }
276
+ }
277
+ return providers;
241
278
  }
242
279
  /** Writes the registry. The key lives here, so the file is 0600. */
243
280
  async save(providers) {
244
281
  await writeJsonAtomic(this.registryPath, { providers });
245
282
  }
246
283
  /**
247
- * Makes Pi's files match the registry.
248
- *
249
- * `previousIds` are the providers Super Pi managed before this change: they are the only ones
250
- * whose entries may be REMOVED. An id that is in Pi's `models.json` and not in `previousIds` belongs
251
- * to the person who wrote that file, and touching it would be Super Pi editing a provider it does
252
- * not own - so the write is refused with a message naming the id.
284
+ * Directly updates Pi's models.json and auth.json to match the new set of providers.
285
+ * Full visual editor semantics: any provider in models.json is editable and removable.
253
286
  */
254
287
  async applyToPi(previous, next) {
255
288
  const previousIds = new Set(previous.map((provider) => provider.id));
@@ -257,15 +290,9 @@ export class ProviderRegistry {
257
290
  const modelsPath = piModelsPath(this.piDir);
258
291
  const existing = await readJsonFile(modelsPath) ?? {};
259
292
  const existingProviders = existing.providers ?? {};
260
- for (const provider of next) {
261
- if (existingProviders[provider.id] !== undefined && !previousIds.has(provider.id)) {
262
- throw new Error(`Pi 的 models.json 里已经有供应商「${provider.id}」,而它不是 Super Pi 添加的,所以没有动它。请换一个供应商 id,或者先在 Pi 的模型配置文件里删掉它。`);
263
- }
264
- }
265
293
  const mergedProviders = { ...existingProviders };
266
294
  for (const provider of next)
267
295
  mergedProviders[provider.id] = toPiProvider(provider);
268
- // The ones we managed and no longer do.
269
296
  for (const id of previousIds)
270
297
  if (!nextIds.has(id))
271
298
  delete mergedProviders[id];
@@ -274,16 +301,6 @@ export class ProviderRegistry {
274
301
  }
275
302
  /**
276
303
  * Writes the keys into Pi's `auth.json`.
277
- *
278
- * A credential is REMOVED only when it is the one Super Pi wrote: the same provider id and the
279
- * same key. A person who replaced our key with their own - or who ran Pi's `/login` and put an
280
- * OAuth token there - keeps it. Deleting somebody else's credential because they removed our
281
- * provider would be the worst version of this feature.
282
- *
283
- * TWO CASES, and the second is the one that is easy to miss: the provider can be gone ENTIRELY
284
- * (it was removed), or it can still be there with its key GONE (the person asked for the key to be
285
- * cleared). Both have to take the credential with them, or "clear the key" would leave a working
286
- * key sitting in Pi's auth file.
287
304
  */
288
305
  async applyKeysToPi(previous, next) {
289
306
  const authPath = piAuthPath(this.piDir);
@@ -295,7 +312,6 @@ export class ProviderRegistry {
295
312
  }
296
313
  for (const provider of previous) {
297
314
  const updated = nextById.get(provider.id);
298
- // The key is still wanted (the provider is unchanged, or it has a new one) - nothing to do.
299
315
  if (updated && updated.apiKey)
300
316
  continue;
301
317
  if (!updated && !provider.apiKey)
@@ -314,7 +330,7 @@ export function publicProviders(providers) {
314
330
  name: provider.name ?? provider.id,
315
331
  baseUrl: provider.baseUrl,
316
332
  api: provider.api,
317
- hasKey: Boolean(provider.apiKey),
333
+ hasKey: Boolean(provider.apiKey || provider.hasAuthKey),
318
334
  models: provider.models.map(model => ({
319
335
  id: model.id,
320
336
  name: model.name ?? model.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.120",
3
+ "version": "0.1.121",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",