pi-rozalia 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -66,6 +66,10 @@ On startup, the extension fetches the model list from the server's `/v1/models`
66
66
 
67
67
  The `/login` flow stores your server URL and API key in Pi's credential store (`~/.pi/agent/auth.json`). On subsequent logins the stored URL is pre-filled, so you only need to re-enter it if your server changes.
68
68
 
69
+ ## Known Limitations
70
+
71
+ - **Models stay listed after `/logout`.** Pi's extension API has no logout callback/event for OAuth-backed providers, so this extension has no way to detect that `/logout` ran and revert the provider to its empty stub. The model list only clears on the next Pi restart. This is a Pi platform limitation, not something this extension can currently work around.
72
+
69
73
  ## TODO
70
74
 
71
75
  - [ ] Multi-server support — register each configured server as its own provider with a derived name (e.g. `rozalia-localhost-1234`), so models from different servers are unambiguous in the picker
@@ -212,6 +212,15 @@ async function registerRozaliaProvider(
212
212
  config.apiKey = apiKey;
213
213
  }
214
214
 
215
+ // Unregister first so Pi replaces the stub (empty models) with
216
+ // the real model list. Without this, registerProvider keeps
217
+ // the stub's models: [] on an already-registered provider.
218
+ try {
219
+ pi.unregisterProvider("rozalia");
220
+ } catch {
221
+ // not previously registered; ignore
222
+ }
223
+
215
224
  pi.registerProvider("rozalia", config);
216
225
  }
217
226
 
@@ -226,8 +235,9 @@ async function registerRozaliaProvider(
226
235
  function createLoginFlow(
227
236
  defaultUrl: string,
228
237
  defaultApiKey: string | undefined,
229
- ): (pi: ExtensionAPI, callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials> {
230
- return async (pi: ExtensionAPI, callbacks: OAuthLoginCallbacks) => {
238
+ pi: ExtensionAPI,
239
+ ): (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials> {
240
+ return async (callbacks: OAuthLoginCallbacks) => {
231
241
  const inputUrl = await callbacks.onPrompt({
232
242
  message: `Enter Rozalia server URL (press Enter for ${defaultUrl}):`,
233
243
  });
@@ -250,7 +260,7 @@ function createLoginFlow(
250
260
  }
251
261
 
252
262
  // Actually register the provider so models appear immediately
253
- const oauthBlock = buildOauthBlock(defaultUrl, defaultApiKey);
263
+ const oauthBlock = buildOauthBlock(defaultUrl, defaultApiKey, pi);
254
264
  await registerRozaliaProvider(pi, creds, oauthBlock);
255
265
 
256
266
  return encodeCreds(creds);
@@ -264,24 +274,43 @@ function createLoginFlow(
264
274
  function buildOauthBlock(
265
275
  defaultUrl: string,
266
276
  defaultApiKey: string | undefined,
277
+ pi: ExtensionAPI,
267
278
  ) {
268
279
  return {
269
280
  name: "Rozalia",
270
- login: createLoginFlow(defaultUrl, defaultApiKey),
271
- refreshToken: async (creds: OAuthCredentials, _signal: AbortSignal) => {
281
+ login: createLoginFlow(defaultUrl, defaultApiKey, pi),
282
+ refreshToken: async (creds: OAuthCredentials, signal: AbortSignal) => {
272
283
  const payload = decodeCreds(creds);
284
+ if (!payload.baseUrl) return creds;
285
+ // Re-register with fresh models so the picker updates without restart
273
286
  try {
274
- const ctrl = new AbortController();
275
- await fetchModels(payload.baseUrl, payload.apiKey, ctrl.signal);
287
+ await registerRozaliaProvider(pi, payload, {
288
+ name: "Rozalia",
289
+ login: createLoginFlow(defaultUrl, defaultApiKey, pi),
290
+ refreshToken: async (c: OAuthCredentials, s: AbortSignal) =>
291
+ refreshTokenRozalia(c, s, payload, defaultUrl, defaultApiKey, pi),
292
+ getApiKey: (c: OAuthCredentials) => decodeCreds(c).apiKey || "",
293
+ });
276
294
  } catch {
277
- // network blip
295
+ // network blip — keep creds, retry on next call
278
296
  }
279
- return creds;
297
+ return encodeCreds(payload);
280
298
  },
281
299
  getApiKey: (creds: OAuthCredentials) => decodeCreds(creds).apiKey || "",
282
300
  };
283
301
  }
284
302
 
303
+ async function refreshTokenRozalia(
304
+ creds: OAuthCredentials,
305
+ _signal: AbortSignal,
306
+ _payload: CredsPayload,
307
+ _defaultUrl: string,
308
+ _defaultApiKey: string | undefined,
309
+ _pi: ExtensionAPI,
310
+ ): Promise<OAuthCredentials> {
311
+ return encodeCreds(decodeCreds(creds));
312
+ }
313
+
285
314
  // ---------------------------------------------------------------------------
286
315
  // Extension entry point
287
316
  // ---------------------------------------------------------------------------
@@ -314,7 +343,9 @@ export default async function (pi: ExtensionAPI) {
314
343
  // No saved credential — will use env vars or prompt
315
344
  }
316
345
 
317
- const oauthBlock = buildOauthBlock(envBaseUrl, envApiKey);
346
+ // Capture pi in a closure so the login flow can call registerRozaliaProvider
347
+ // (Pi only passes callbacks to login, not pi).
348
+ const oauthBlock = buildOauthBlock(envBaseUrl, envApiKey, pi);
318
349
 
319
350
  // Initial stub registration so "Rozalia" appears in /login selector
320
351
  pi.registerProvider("rozalia", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-rozalia",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "OpenAI-compatible provider extension for the Pi coding agent — connects to Rozalia AI",
5
5
  "keywords": [
6
6
  "pi-package",