pi-rozalia 0.1.4 → 0.2.1
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 +1 -2
- package/extensions/index.ts +85 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-rozalia
|
|
2
2
|
|
|
3
|
-
An extension for the [Pi coding agent](https://github.com/
|
|
3
|
+
An extension for the [Pi coding agent](https://github.com/earendil-works/pi) that connects to a [Rozalia AI](https://ai.zygoon.pl) server via the OpenAI-compatible Chat Completions API.
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
@@ -34,7 +34,6 @@ The server URL and API key are stored in `~/.pi/agent/auth.json` and reused on s
|
|
|
34
34
|
| Variable | Description | Default |
|
|
35
35
|
|----------|-------------|---------|
|
|
36
36
|
| `ROZALIA_BASE_URL` | Rozalia AI server base URL | `https://ai.zygoon.pl/v1` |
|
|
37
|
-
| `ROZALIA_BASE_URL` | Rozalia AI server base URL | `https://ai.zygoon.pl/v1` |
|
|
38
37
|
| `ROZALIA_API_KEY` | API key for authentication | *(none)* |
|
|
39
38
|
| `ROZALIA_TIMEOUT` | Model discovery timeout in ms | `5000` |
|
|
40
39
|
|
package/extensions/index.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
31
31
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
32
|
+
import type { RefreshModelsContext } from "@earendil-works/pi-ai";
|
|
32
33
|
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
34
35
|
// Credential payload (stored in OAuth refresh field).
|
|
@@ -198,6 +199,78 @@ function logDiscoveryError(baseUrl: string, error: unknown): void {
|
|
|
198
199
|
);
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// TTL cache for model discovery
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
let cachedModels: ProviderModelConfig[] = [];
|
|
207
|
+
let lastFetchedAt = 0;
|
|
208
|
+
const MODEL_CACHE_TTL_MS = 5 * 1000; // 5 seconds — debounce only; /v1/models is cheap on a self-hosted server and the coordinator already dedupes concurrent calls
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolve baseUrl and apiKey from Pi's credential context.
|
|
212
|
+
*
|
|
213
|
+
* Priority: oauth credential > env vars.
|
|
214
|
+
* This is called by Pi during its refresh cycle — the credential is the
|
|
215
|
+
* effective configured auth (OAuth stored in ~/.pi/agent/auth.json or
|
|
216
|
+
* env-based API key).
|
|
217
|
+
*/
|
|
218
|
+
function resolveAuthFromContext(context: RefreshModelsContext): { baseUrl: string; apiKey: string } {
|
|
219
|
+
let baseUrl = getEnvBaseUrl();
|
|
220
|
+
let apiKey = getEnvApiKey();
|
|
221
|
+
|
|
222
|
+
const cred = context.credential;
|
|
223
|
+
if (cred?.type === "oauth") {
|
|
224
|
+
const p = decodeCreds(cred as OAuthCredentials);
|
|
225
|
+
if (p.baseUrl) baseUrl = p.baseUrl;
|
|
226
|
+
if (p.apiKey) apiKey = p.apiKey;
|
|
227
|
+
} else if (cred?.type === "api_key" && typeof cred.key === "string") {
|
|
228
|
+
apiKey = apiKey || cred.key;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { baseUrl, apiKey };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The `refreshModels` hook — called by Pi on every refresh cycle (startup,
|
|
236
|
+
* explicit model refresh, etc.). Implements a TTL cache to avoid hammering
|
|
237
|
+
* the server while still responding to real changes within 5 minutes.
|
|
238
|
+
*/
|
|
239
|
+
async function refreshRozaliaModels(context: RefreshModelsContext): Promise<ProviderModelConfig[]> {
|
|
240
|
+
// TTL cache: skip fetch if we have a fresh list and caller didn't force
|
|
241
|
+
if (context.allowNetwork && !context.force && cachedModels.length > 0) {
|
|
242
|
+
const age = Date.now() - lastFetchedAt;
|
|
243
|
+
if (age < MODEL_CACHE_TTL_MS) {
|
|
244
|
+
return cachedModels;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const { baseUrl, apiKey } = resolveAuthFromContext(context);
|
|
249
|
+
if (!baseUrl) {
|
|
250
|
+
return cachedModels.length > 0 ? cachedModels : getFallbackModels();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// No API key means the user is logged out — skip fetch, return stub.
|
|
254
|
+
if (!apiKey) {
|
|
255
|
+
return cachedModels.length > 0 ? cachedModels : getFallbackModels();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
const fetched = await fetchModels(baseUrl, apiKey, controller.signal);
|
|
261
|
+
if (fetched.length > 0) {
|
|
262
|
+
cachedModels = fetched;
|
|
263
|
+
lastFetchedAt = Date.now();
|
|
264
|
+
return fetched;
|
|
265
|
+
}
|
|
266
|
+
return getFallbackModels();
|
|
267
|
+
} catch (error) {
|
|
268
|
+
logDiscoveryError(baseUrl, error);
|
|
269
|
+
// Return cached models if available (don't replace with fallback on transient failure)
|
|
270
|
+
return cachedModels.length > 0 ? cachedModels : getFallbackModels();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
201
274
|
// ---------------------------------------------------------------------------
|
|
202
275
|
// Provider registration helper
|
|
203
276
|
// ---------------------------------------------------------------------------
|
|
@@ -215,6 +288,9 @@ async function registerRozaliaProvider(
|
|
|
215
288
|
const { baseUrl, apiKey } = creds;
|
|
216
289
|
if (!baseUrl) return;
|
|
217
290
|
|
|
291
|
+
// No API key — user is logged out, keep the stub.
|
|
292
|
+
if (!apiKey) return;
|
|
293
|
+
|
|
218
294
|
let models: ProviderModelConfig[];
|
|
219
295
|
try {
|
|
220
296
|
const controller = new AbortController();
|
|
@@ -313,7 +389,7 @@ function buildOauthBlock(
|
|
|
313
389
|
refreshToken: async (creds: OAuthCredentials) => {
|
|
314
390
|
const payload = decodeCreds(creds);
|
|
315
391
|
if (!payload.baseUrl) return creds;
|
|
316
|
-
//
|
|
392
|
+
// Refresh the model list so the picker updates without restart.
|
|
317
393
|
// Reuse this same oauthBlock instead of reconstructing it — only the
|
|
318
394
|
// model list changes, not the auth callbacks.
|
|
319
395
|
try {
|
|
@@ -340,25 +416,27 @@ export default async function (pi: ExtensionAPI) {
|
|
|
340
416
|
// (Pi only passes callbacks to login, not pi).
|
|
341
417
|
const oauthBlock = buildOauthBlock(envBaseUrl, envApiKey, pi);
|
|
342
418
|
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
419
|
+
// Register the provider with refreshModels so Pi populates models on startup.
|
|
420
|
+
// The refreshModels hook is called by Pi during its refresh cycle (startup,
|
|
421
|
+
// explicit model refresh, etc.) with the resolved credential — this is the
|
|
422
|
+
// canonical Pi mechanism for dynamic model discovery.
|
|
346
423
|
pi.registerProvider("rozalia", {
|
|
347
424
|
name: "Rozalia",
|
|
348
425
|
baseUrl: envBaseUrl,
|
|
349
426
|
api: "openai-completions",
|
|
350
427
|
authHeader: true,
|
|
351
|
-
models: [],
|
|
428
|
+
models: [], // Stub — refreshModels will populate the real list
|
|
429
|
+
refreshModels: refreshRozaliaModels,
|
|
352
430
|
oauth: oauthBlock,
|
|
353
431
|
});
|
|
354
432
|
|
|
355
433
|
// Best-effort: if the API key is provided via env var, register eagerly so
|
|
356
|
-
// models appear without waiting for
|
|
434
|
+
// models appear immediately without waiting for Pi's refresh cycle.
|
|
357
435
|
if (envApiKey) {
|
|
358
436
|
try {
|
|
359
437
|
await registerRozaliaProvider(pi, { baseUrl: envBaseUrl, apiKey: envApiKey }, oauthBlock);
|
|
360
438
|
} catch {
|
|
361
|
-
// ignore — will retry on next
|
|
439
|
+
// ignore — will retry on next Pi refresh cycle
|
|
362
440
|
}
|
|
363
441
|
}
|
|
364
442
|
}
|