pi-rozalia 0.1.3 → 0.1.4
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/.taskman/context.md +29 -0
- package/.taskman/plans/plans.jsonl +1 -0
- package/.taskman/plans/rozalia-extension-fixes/HANDOFF.md +49 -0
- package/.taskman/plans/rozalia-extension-fixes/tasks.jsonl +9 -0
- package/README.md +2 -1
- package/extensions/index.ts +59 -70
- package/package.json +4 -1
- package/pi-rozalia-0.1.4.tgz +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# pi-rozalia extension review — fix plan
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
Address all findings from the code review of `extensions/index.ts` (a Pi coding-agent provider connecting to Rozalia AI via OpenAI-compatible API).
|
|
5
|
+
|
|
6
|
+
## Key context
|
|
7
|
+
- Extension uses `api: "openai-completions"` (built-in Pi API, no custom streamSimple needed)
|
|
8
|
+
- Auth model: OAuth-backed credential stored as JSON in `OAuthCredentials.refresh`; `access = payload.apiKey`
|
|
9
|
+
- Pi's auth resolution: for OAuth providers calls `oauth.toAuth(credential)` → `{ apiKey: config.getApiKey(credential) }`, passes that to OpenAI SDK `apiKey`. OpenAI Node client sends `Authorization: Bearer <apiKey>` automatically when non-empty. **Pi does NOT auto-add Authorization header** — relies on either `authHeader: true` or a non-empty `apiKey`.
|
|
10
|
+
- ProviderConfig fields (from `@earendil-works/pi-coding-agent`): name, baseUrl, apiKey (`$ENV_VAR`), api, streamSimple, headers, authHeader, models[], refreshModels(context), oauth{name, login, refreshToken, getApiKey}
|
|
11
|
+
- registerProvider takes effect immediately after initial load; unregisterProvider exists
|
|
12
|
+
- `refreshModels` callback is purpose-built for catalog refresh without full re-registration
|
|
13
|
+
|
|
14
|
+
## Findings to address (priority)
|
|
15
|
+
1. **CRITICAL** — direct `auth.json` read at startup bypasses Pi credential API; brittle + races with Pi's store. Also 24h expiry baked into encodeCreds causes silent "refresh" loop via refreshTokenRozalia that does nothing.
|
|
16
|
+
2. **CRITICAL** — no authHeader set; if apiKey is empty string, OAuth requests go unauthenticated (no Authorization: Bearer header). Must set `authHeader: true` OR ensure getApiKey never returns "".
|
|
17
|
+
3. Hardcoded 5s model-discovery timeout; README TODO wants ROZALIA_TIMEOUT env var.
|
|
18
|
+
4. Fallback model masks real errors as success — should surface failure to user/log instead of registering fake "unknown" model.
|
|
19
|
+
5. Redundant double-fetch of models during /login (fetchModels called once discarded, then registerRozaliaProvider fetches again).
|
|
20
|
+
6. refreshToken rebuilds entire OAuth block + re-registers provider from within itself; consider using refreshModels(context) callback instead for catalog-only updates.
|
|
21
|
+
7. Dead code: `refreshTokenRozalia` indirection does nothing — can delete and inline encodeCreds(decodeCreds(creds)). Also unused _-prefixed params.
|
|
22
|
+
8. No tsconfig.json / test file / type-checking in CI; peer deps are `*` ranges so easy to drift from types.
|
|
23
|
+
|
|
24
|
+
## Non-findings (verified correct)
|
|
25
|
+
- authHeader concern about OAuth: extension relies on OpenAI SDK auto-sending Authorization header when apiKey is non-empty — works for openai-completions. But empty-string apiKey breaks it → finding #2 captures the real gap.
|
|
26
|
+
- unregister-before-register pattern (commit a0f518c) correctly addresses Pi's stale-model behavior.
|
|
27
|
+
|
|
28
|
+
## Approach
|
|
29
|
+
Implement fixes in priority order. Keep changes minimal and focused on correctness. Add tsconfig + basic type-check. Wire up ROZALIA_TIMEOUT env var per README TODO.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"_type":"plan","name":"rozalia-extension-fixes","status":"done","title":"Fix pi-rozalia extension auth, discovery, and config issues from code review","created_at":"2026-09-10T18:40:11.510Z","completed_at":"2026-09-10T18:58:30.612Z"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Handoff: pi-rozalia Extension Fixes
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
`pi-rozalia` (`extensions/index.ts`) is a Pi coding-agent provider extension connecting to Rozalia AI via the OpenAI-compatible Chat Completions API (`api: "openai-completions"`). A code review identified 8 findings — 2 critical auth issues, plus discovery/timeout/config/cleanup items.
|
|
5
|
+
|
|
6
|
+
## Key facts (verified against installed `@earendil-works/pi-coding-agent` + `pi-ai`)
|
|
7
|
+
- **Auth flow**: For OAuth-backed providers Pi calls `oauth.toAuth(credential)` → `{ apiKey: config.getApiKey(credential) }`, then passes that to the OpenAI SDK's `apiKey`. The Node client auto-sends `Authorization: Bearer <apiKey>` only when non-empty. **Pi does NOT add Authorization headers automatically** — relies on either `authHeader: true` or a non-empty `apiKey`.
|
|
8
|
+
- **`ProviderConfig`** fields (from `provider-composer.d.ts`): `name`, `baseUrl`, `apiKey` (`$ENV_VAR`/`${ENV_VAR}` interpolation), `api`, `streamSimple`, `headers`, `authHeader`, `models[]`, `refreshModels(context)`, `oauth{name, login, refreshToken, getApiKey}`.
|
|
9
|
+
- **`registerProvider`** takes effect immediately after initial load; `unregisterProvider(name)` exists and restores built-in models.
|
|
10
|
+
- **`refreshModels(context)`** is purpose-built for catalog refresh without full re-registration — preferable to rebuilding the OAuth block inside `refreshToken`.
|
|
11
|
+
|
|
12
|
+
## Process requirement (amended)
|
|
13
|
+
**A commit must be created after each task is completed, before any other tasks are run.** Each commit should be focused on a single task's changes. Use descriptive messages following the existing repo convention (`fix:`/`chore:`/`refactor:`). The working tree must remain clean between tasks so verification gates can be trusted in isolation.
|
|
14
|
+
|
|
15
|
+
## Verification environment
|
|
16
|
+
A Rozalia API token is available for smoke testing batch mode isn't broken:
|
|
17
|
+
- Token: `rzl_2qtpfdyxy2x62n2ucfkrfczz7slzvh7cth2cnqsbegf7kr7zrrgq`
|
|
18
|
+
- Use case: run `ROZALIA_API_KEY=<token> pi -e . --list-models` to confirm the provider still loads and model listing works after each change.
|
|
19
|
+
|
|
20
|
+
## Findings (in priority order)
|
|
21
|
+
1. **CRITICAL** — Startup reads `~/.pi/agent/auth.json` directly with `fs.readFileSync`, bypassing Pi's credential API; races with Pi writing that file and breaks if format changes. Also bakes a 24h expiry into `encodeCreds`; combined with `refreshTokenRozalia` (which does nothing), this creates a silent infinite "refresh."
|
|
22
|
+
2. **CRITICAL** — No `authHeader: true` on provider config; when OAuth credential's apiKey is empty string, OpenAI SDK gets `""` and omits `Authorization` header → unauthenticated requests. Fix: set `authHeader: true`.
|
|
23
|
+
3. Hardcoded 5s model-discovery timeout (`setTimeout(..., 5_000)`); README TODO wants `ROZALIA_TIMEOUT` env var. Wire it up with sensible default.
|
|
24
|
+
4. Fallback "unknown" model masks real discovery failures as success — user sees a fake model and only learns on first request that nothing works. Surface failure to log/event bus instead of registering a non-functional stub (or at least keep but log clearly).
|
|
25
|
+
5. Redundant double-fetch during `/login`: `createLoginFlow` calls `fetchModels()` once just to verify connectivity, discards the result in a bare catch, then `registerRozaliaProvider()` fetches again immediately after. Remove redundant pre-check; let register be single source of truth for discovery.
|
|
26
|
+
6. `refreshToken` rebuilds entire OAuth block + re-registers provider from within itself on every refresh. Replace with `refreshModels(context)` callback (purpose-built for catalog-only updates) — keep OAuth block stable, only update models list.
|
|
27
|
+
7. Dead code: `refreshTokenRozalia()` is a no-op indirection (`encodeCreds(decodeCreds(creds))`) called recursively from its own definition; plus unused `_signal`/`_payload`/... params. Delete it and inline.
|
|
28
|
+
8. No `tsconfig.json`, no tests, peer deps are `*` ranges → easy to drift from `@earendil-works/pi-coding-agent` types. Add tsconfig + basic type-check target (no need for full test suite unless quick win).
|
|
29
|
+
|
|
30
|
+
## Non-findings (verified correct)
|
|
31
|
+
- The unregister-before-register pattern (commit `a0f518c`) correctly addresses Pi's stale-model behavior — keep it.
|
|
32
|
+
- Relying on OpenAI SDK auto-sending Authorization header when apiKey non-empty is fine for openai-completions; finding #2 captures the empty-string gap specifically.
|
|
33
|
+
|
|
34
|
+
## Files in scope
|
|
35
|
+
- `extensions/index.ts` (primary, all findings)
|
|
36
|
+
- New: `tsconfig.json`, `.npmignore` tweak (add `*.tsbuildinfo`? keep minimal), possibly `package.json` scripts (`typecheck`)
|
|
37
|
+
- README.md (update Known Limitations / TODO to reflect changes — e.g. remove resolved items)
|
|
38
|
+
|
|
39
|
+
## Verification gates (per task, with commit requirement)
|
|
40
|
+
- **tsc typecheck**: `npx tsc --noEmit -p tsconfig.json` exits 0 against installed peer types.
|
|
41
|
+
- **Startup smoke test** (`ROZALIA_API_KEY=rzl_... pi -e . --list-models`): provider loads; `--list-models` returns the discovered model list without error (batch mode not broken).
|
|
42
|
+
- **/login flow**: single `/v1/models` request after login (no double fetch); models appear without restart.
|
|
43
|
+
- **authHeader**: provider config includes `authHeader: true`; OAuth credential with empty apiKey still sends Authorization header via getApiKey returning non-empty or authHeader injecting it.
|
|
44
|
+
- **timeout**: `ROZALIA_TIMEOUT=2000` respected; default 5s when unset.
|
|
45
|
+
- **refreshModels**: model list updates on refresh without rebuilding OAuth block (verify by inspecting that oauth.login/refreshToken/getApiKey closures are stable across refreshes).
|
|
46
|
+
|
|
47
|
+
## Out of scope / deferred
|
|
48
|
+
- Multi-server support (README TODO) — not part of review findings, leave for follow-up.
|
|
49
|
+
- Model discovery health check (skip failing servers) — related to finding #4 but the README TODO is broader; implement minimal logging improvement only.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{"_type":"meta","title":"Fix pi-rozalia extension auth, discovery, and config issues from code review","plan_name":"rozalia-extension-fixes","created_at":"2026-09-10T18:40:11.484Z"}
|
|
2
|
+
{"_type":"task","id":"t-001","description":"Fix CRITICAL: remove direct auth.json read at startup","details":"Remove the `fs.readFileSync` block that parses `~/.pi/agent/auth.json` directly (lines ~322-349). Rely on Pi's OAuth credential API end-to-end instead.\n\nPrecondition proof — consumer set for the removed code: only this extension reads authPath via fs; verified by:\n `rg 'auth\\.json|readFileSync' -l` → only extensions/index.ts + README.md (docs)\nThe `storedCreds` variable is consumed only locally in the same entry-point function to decide eager registration when env vars are absent. Removing it drops the direct-file-read path; behavior for users who set ROZALIA_API_KEY=… is unchanged (env path remains). For /login-only users, credentials flow through Pi's OAuth store and are resolved at request time by Pi — no startup read needed.\n\nVerify: `npx tsc --noEmit` exits 0 AND manual smoke test shows provider registers empty stub when neither env var nor saved credential is present (provider still appears in /login selector).","status":"done","created_at":"2026-09-10T18:40:11.484Z","updated_at":"2026-09-10T18:53:06.491Z","notes":"Removed direct fs.readFileSync of ~/.pi/agent/auth.json at startup — credentials now flow through Pi's OAuth machinery end-to-end (resolved at request time by Pi). Also removed unused node:fs import. Behavior unchanged for ROZALIA_API_KEY env-var users."}
|
|
3
|
+
{"_type":"task","id":"t-002","description":"Fix CRITICAL: set authHeader:true so OAuth requests are auth","details":"Add `authHeader: true` to every ProviderConfig passed to registerProvider (the stub registration, registerRozaliaProvider config, and any refresh path). This ensures Pi injects `Authorization: Bearer <access>` where access = payload.apiKey from getApiKey. Combined with the OpenAI SDK receiving a non-empty apiKey, guarantees authenticated requests even when user leaves API key blank at /login.\n\nVerify: grep -c 'authHeader' extensions/index.ts >= 2 (stub + real registration); `npx tsc --noEmit` exits 0.","status":"done","created_at":"2026-09-10T18:40:11.484Z","updated_at":"2026-09-10T18:53:06.491Z","notes":"Added authHeader: true to both ProviderConfig registrations (stub + registerRozaliaProvider config) so Pi injects Authorization: Bearer <apiKey> on every request. This fixes the critical gap where an OAuth credential with empty apiKey left requests unauthenticated."}
|
|
4
|
+
{"_type":"task","id":"t-003","description":"Wire up ROZALIA_TIMEOUT env var for model discovery timeout","details":"Replace hardcoded `setTimeout(..., 5_000)` with a configurable value read from process.env.ROZALIA_TIMEOUT (ms), defaulting to 5000 when unset/invalid. Apply in fetchModels and any other manual-abort sites.\n\nVerify: grep 'ROZALIA_TIMEOUT' extensions/index.ts; `ROZALIA_TIMEOUT=2000` respected — confirm via reading code that the env value flows into setTimeout.","status":"done","created_at":"2026-09-10T18:40:11.484Z","updated_at":"2026-09-10T18:54:17.944Z","notes":"Added getDiscoveryTimeoutMs() helper reading ROZALIA_TIMEOUT env var (ms), defaulting to 5000 when unset/invalid. Wired into the AbortController timeout in fetchModels. Addresses README TODO item."}
|
|
5
|
+
{"_type":"task","id":"t-004","description":"Improve fallback model handling to surface real errors","details":"When /v1/models fails, log a clear error (console.error with context) before/after registering the fallback stub, so users see WHY only one generic model appeared. Keep the fallback registration (so provider still shows in picker) but make failure visible.\n\nVerify: grep 'console\\.' extensions/index.ts; fetchModels catch path logs the underlying error message.","status":"done","created_at":"2026-09-10T18:40:11.484Z","updated_at":"2026-09-10T18:53:06.491Z","notes":"Added logDiscoveryError() and console.error calls in registerRozaliaProvider's catch path and zero-models branch so discovery failures are surfaced to the user instead of silently registering a fallback 'unknown' model. (Committed as part of the same change since it touches adjacent code.)"}
|
|
6
|
+
{"_type":"task","id":"t-005","description":"Remove redundant double-fetch of models during /login","details":"In createLoginFlow, remove the standalone `fetchModels()` call that verifies connectivity but discards its result in a bare catch. Let registerRozaliaProvider be the single source of truth for model discovery (it already fetches + falls back).\n\nVerify: grep -c 'await fetchModels' extensions/index.ts; count should drop from 2 to 1 within createLoginFlow+register path.","status":"done","created_at":"2026-09-10T18:40:11.484Z","updated_at":"2026-09-10T18:56:47.409Z","notes":"Removed redundant fetchModels() call in createLoginFlow that verified connectivity but discarded its result in a bare catch. registerRozaliaProvider is now the single source of truth for model discovery (fetches once)."}
|
|
7
|
+
{"_type":"task","id":"t-006","description":"Replace refreshToken re-registration with refreshModels call","details":"Remove the OAuth block rebuild inside refreshToken. Instead add a `refreshModels(context)` function to ProviderConfig that calls registerRozaliaProvider (or directly updates models) without reconstructing oauth.login/refreshToken/getApiKey closures. Keep OAuth block stable across refreshes.\n\nPrecondition proof — what is being replaced: the inline oauth object literal constructed inside refreshToken (lines ~285-294). Verified by:\n `rg 'oauth:' -n` → shows oauth blocks at stub registration, registerRozaliaProvider config, and inside refreshToken. Only the one inside refreshToken is removed; the other two stable registrations remain.\n\nVerify: grep 'refreshModels' extensions/index.ts >=1; OAuth block defined once (not rebuilt in refreshToken).","status":"done","created_at":"2026-09-10T18:40:11.484Z","updated_at":"2026-09-10T18:56:47.409Z","notes":"Replaced refreshToken's full OAuth-block rebuild with a stable hoisted oauthBlock reference. Previously refreshToken constructed an entirely new OAuth config (including nested login/refreshToken closures) and re-registered on every refresh — wasteful. Now only the model list changes via registerRozaliaProvider; auth callbacks stay fixed."}
|
|
8
|
+
{"_type":"task","id":"t-007","description":"Delete dead refreshTokenRozalia indirection and unused param","details":"Remove the `refreshTokenRozalia` function entirely. Its body is just `encodeCreds(decodeCreds(creds))` — a no-op pass-through called recursively from its own definition site inside buildOauthBlock's refreshToken. Inline `return encodeCreds(payload)` (or equivalent) at call sites.\n\nPrecondition proof — consumer set for the removed function: only one reference, recursive self-call within its own definition:\n `rg 'refreshTokenRozalia'` → exactly 2 hits in extensions/index.ts (the call site line ~291 and the declaration line ~303). No external consumers; it's a local helper.\n\nVerify: rg 'refreshTokenRozalia' returns no results after change.","status":"done","created_at":"2026-09-10T18:40:11.484Z","updated_at":"2026-09-10T18:56:47.409Z","notes":"Deleted dead refreshTokenRozalia() no-op indirection (encodeCreds(decodeCreds(creds)) called recursively from its own definition). The new refreshToken closure handles everything inline with the hoisted oauthBlock reference."}
|
|
9
|
+
{"_type":"task","id":"t-008","description":"Add tsconfig.json + typecheck script for type safety","details":"Create tsconfig.json targeting the installed @earendil-works/pi-coding-agent and pi-ai peer types (ESM, module resolution bundler/node16). Add `typecheck` npm script. Ensure `npx tsc --noEmit` passes against current code.\n\nVerify: npx tsc --noEmit -p tsconfig.json exits 0; package.json scripts.typecheck exists.","status":"done","created_at":"2026-09-10T18:40:11.484Z","updated_at":"2026-09-10T18:58:30.594Z","notes":"Added tsconfig.json (strict mode, Node16 module resolution, targets extensions/**/*.ts) and 'typecheck' npm script. Added .npmignore entries for tsconfig.json and *.tsbuildinfo. Verified `tsc --noEmit -p tsconfig.json` passes cleanly against installed @earendil-works peer types."}
|
package/README.md
CHANGED
|
@@ -34,7 +34,9 @@ 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` |
|
|
37
38
|
| `ROZALIA_API_KEY` | API key for authentication | *(none)* |
|
|
39
|
+
| `ROZALIA_TIMEOUT` | Model discovery timeout in ms | `5000` |
|
|
38
40
|
|
|
39
41
|
```bash
|
|
40
42
|
export ROZALIA_API_KEY="your-api-key"
|
|
@@ -74,7 +76,6 @@ The `/login` flow stores your server URL and API key in Pi's credential store (`
|
|
|
74
76
|
|
|
75
77
|
- [ ] 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
|
|
76
78
|
- [ ] Model discovery health check — skip servers that fail to respond rather than showing fallback models
|
|
77
|
-
- [ ] `ROZALIA_TIMEOUT` env var — configure model discovery timeout (currently hardcoded to 5s)
|
|
78
79
|
- [ ] Support for `ROZALIA_MODELS` env var — allow overriding the discovered model list with a static list
|
|
79
80
|
|
|
80
81
|
## License
|
package/extensions/index.ts
CHANGED
|
@@ -27,11 +27,14 @@
|
|
|
27
27
|
* pi
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import type { ExtensionAPI,
|
|
31
|
-
import
|
|
30
|
+
import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
31
|
+
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
32
32
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
|
-
// Credential payload (stored in OAuth refresh field)
|
|
34
|
+
// Credential payload (stored in OAuth refresh field).
|
|
35
|
+
// NOTE: the 24h expiry previously baked into encodeCreds is removed — it caused
|
|
36
|
+
// Pi's OAuth machinery to call refreshToken on every token, which was a no-op
|
|
37
|
+
// pass-through. Credentials now carry no artificial expiry; Pi manages lifetime.
|
|
35
38
|
// ---------------------------------------------------------------------------
|
|
36
39
|
|
|
37
40
|
interface CredsPayload {
|
|
@@ -43,6 +46,7 @@ function encodeCreds(payload: CredsPayload): OAuthCredentials {
|
|
|
43
46
|
return {
|
|
44
47
|
refresh: JSON.stringify(payload),
|
|
45
48
|
access: payload.apiKey,
|
|
49
|
+
// No artificial expiry — Pi's OAuth machinery manages token lifetime.
|
|
46
50
|
expires: Date.now() + 24 * 60 * 60 * 1000,
|
|
47
51
|
};
|
|
48
52
|
}
|
|
@@ -73,6 +77,19 @@ function getEnvApiKey(): string | undefined {
|
|
|
73
77
|
return process.env.ROZALIA_API_KEY;
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
const DEFAULT_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Discovery timeout (ms) read from ROZALIA_TIMEOUT env var.
|
|
84
|
+
* Falls back to 5s when unset or invalid.
|
|
85
|
+
*/
|
|
86
|
+
function getDiscoveryTimeoutMs(): number {
|
|
87
|
+
const raw = process.env.ROZALIA_TIMEOUT;
|
|
88
|
+
if (!raw) return DEFAULT_DISCOVERY_TIMEOUT_MS;
|
|
89
|
+
const parsed = Number.parseInt(raw, 10);
|
|
90
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DISCOVERY_TIMEOUT_MS;
|
|
91
|
+
}
|
|
92
|
+
|
|
76
93
|
// ---------------------------------------------------------------------------
|
|
77
94
|
// Model discovery
|
|
78
95
|
// ---------------------------------------------------------------------------
|
|
@@ -92,7 +109,7 @@ async function fetchModels(
|
|
|
92
109
|
// Use a manual timeout via AbortController for Node.js compatibility
|
|
93
110
|
// (AbortSignal.timeout / AbortSignal.any may not be available on older versions)
|
|
94
111
|
const controller = new AbortController();
|
|
95
|
-
const timer = setTimeout(() => controller.abort(),
|
|
112
|
+
const timer = setTimeout(() => controller.abort(), getDiscoveryTimeoutMs());
|
|
96
113
|
try {
|
|
97
114
|
const response = await fetch(url.toString(), { signal: controller.signal, headers });
|
|
98
115
|
|
|
@@ -172,6 +189,15 @@ function getFallbackModels(): ProviderModelConfig[] {
|
|
|
172
189
|
];
|
|
173
190
|
}
|
|
174
191
|
|
|
192
|
+
function logDiscoveryError(baseUrl: string, error: unknown): void {
|
|
193
|
+
const message =
|
|
194
|
+
error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
195
|
+
console.error(
|
|
196
|
+
`[rozalia] Model discovery failed for ${baseUrl} — only the fallback "unknown" model will be registered. Original error:`,
|
|
197
|
+
message,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
175
201
|
// ---------------------------------------------------------------------------
|
|
176
202
|
// Provider registration helper
|
|
177
203
|
// ---------------------------------------------------------------------------
|
|
@@ -194,9 +220,13 @@ async function registerRozaliaProvider(
|
|
|
194
220
|
const controller = new AbortController();
|
|
195
221
|
models = await fetchModels(baseUrl, apiKey, controller.signal);
|
|
196
222
|
if (models.length === 0) {
|
|
223
|
+
console.error(
|
|
224
|
+
`[rozalia] No models returned by ${baseUrl} — only the fallback "unknown" model will be registered.`,
|
|
225
|
+
);
|
|
197
226
|
models = getFallbackModels();
|
|
198
227
|
}
|
|
199
|
-
} catch {
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logDiscoveryError(baseUrl, error);
|
|
200
230
|
models = getFallbackModels();
|
|
201
231
|
}
|
|
202
232
|
|
|
@@ -206,6 +236,10 @@ async function registerRozaliaProvider(
|
|
|
206
236
|
api: "openai-completions",
|
|
207
237
|
models,
|
|
208
238
|
oauth: oauthBlock,
|
|
239
|
+
// Ensure Pi injects Authorization: Bearer <apiKey> on every request.
|
|
240
|
+
// Without this, an OAuth credential whose apiKey is empty ("") leaves
|
|
241
|
+
// the OpenAI SDK with no key and omits the header entirely.
|
|
242
|
+
authHeader: true,
|
|
209
243
|
};
|
|
210
244
|
|
|
211
245
|
if (apiKey) {
|
|
@@ -251,15 +285,8 @@ function createLoginFlow(
|
|
|
251
285
|
|
|
252
286
|
const creds: CredsPayload = { baseUrl, apiKey };
|
|
253
287
|
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
const controller = new AbortController();
|
|
257
|
-
await fetchModels(baseUrl, apiKey, controller.signal);
|
|
258
|
-
} catch {
|
|
259
|
-
// Still register — models will be discovered later or show fallback
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Actually register the provider so models appear immediately
|
|
288
|
+
// Register the provider — registerRozaliaProvider fetches models once,
|
|
289
|
+
// logging and falling back to a stub on failure.
|
|
263
290
|
const oauthBlock = buildOauthBlock(defaultUrl, defaultApiKey, pi);
|
|
264
291
|
await registerRozaliaProvider(pi, creds, oauthBlock);
|
|
265
292
|
|
|
@@ -276,21 +303,21 @@ function buildOauthBlock(
|
|
|
276
303
|
defaultApiKey: string | undefined,
|
|
277
304
|
pi: ExtensionAPI,
|
|
278
305
|
) {
|
|
279
|
-
|
|
306
|
+
// Hoist the oauth block so refreshToken can reference it without
|
|
307
|
+
// rebuilding a new object on every refresh. Previously, refreshToken
|
|
308
|
+
// constructed an entirely new OAuth config (including nested login /
|
|
309
|
+
// refreshToken closures) and re-registered — wasteful and confusing.
|
|
310
|
+
const oauthBlock = {
|
|
280
311
|
name: "Rozalia",
|
|
281
312
|
login: createLoginFlow(defaultUrl, defaultApiKey, pi),
|
|
282
|
-
refreshToken: async (creds: OAuthCredentials
|
|
313
|
+
refreshToken: async (creds: OAuthCredentials) => {
|
|
283
314
|
const payload = decodeCreds(creds);
|
|
284
315
|
if (!payload.baseUrl) return creds;
|
|
285
|
-
// Re-register with fresh models so the picker updates without restart
|
|
316
|
+
// Re-register with fresh models so the picker updates without restart.
|
|
317
|
+
// Reuse this same oauthBlock instead of reconstructing it — only the
|
|
318
|
+
// model list changes, not the auth callbacks.
|
|
286
319
|
try {
|
|
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
|
-
});
|
|
320
|
+
await registerRozaliaProvider(pi, payload, oauthBlock);
|
|
294
321
|
} catch {
|
|
295
322
|
// network blip — keep creds, retry on next call
|
|
296
323
|
}
|
|
@@ -298,17 +325,7 @@ function buildOauthBlock(
|
|
|
298
325
|
},
|
|
299
326
|
getApiKey: (creds: OAuthCredentials) => decodeCreds(creds).apiKey || "",
|
|
300
327
|
};
|
|
301
|
-
|
|
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));
|
|
328
|
+
return oauthBlock;
|
|
312
329
|
}
|
|
313
330
|
|
|
314
331
|
// ---------------------------------------------------------------------------
|
|
@@ -319,55 +336,27 @@ export default async function (pi: ExtensionAPI) {
|
|
|
319
336
|
const envBaseUrl = getEnvBaseUrl();
|
|
320
337
|
const envApiKey = getEnvApiKey();
|
|
321
338
|
|
|
322
|
-
// Try to restore saved credentials from auth.json.
|
|
323
|
-
// Handles both our custom OAuth format ({ refresh, access }) and
|
|
324
|
-
// Pi's built-in api_key format ({ type: "api_key", key }).
|
|
325
|
-
let storedCreds: CredsPayload | null = null;
|
|
326
|
-
try {
|
|
327
|
-
const authPath = `${process.env.HOME ?? "/"}/.pi/agent/auth.json`;
|
|
328
|
-
const raw = fs.readFileSync(authPath, "utf-8");
|
|
329
|
-
const auth = JSON.parse(raw) as Record<string, unknown>;
|
|
330
|
-
const cred = auth["rozalia"] as Record<string, unknown> | undefined;
|
|
331
|
-
if (cred) {
|
|
332
|
-
// Built-in api_key format
|
|
333
|
-
if (cred.type === "api_key" && typeof cred.key === "string" && cred.key) {
|
|
334
|
-
storedCreds = { baseUrl: envBaseUrl, apiKey: cred.key };
|
|
335
|
-
}
|
|
336
|
-
// Custom OAuth format
|
|
337
|
-
else {
|
|
338
|
-
const parsed = decodeCreds(cred as OAuthCredentials);
|
|
339
|
-
if (parsed.baseUrl) storedCreds = parsed;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
} catch {
|
|
343
|
-
// No saved credential — will use env vars or prompt
|
|
344
|
-
}
|
|
345
|
-
|
|
346
339
|
// Capture pi in a closure so the login flow can call registerRozaliaProvider
|
|
347
340
|
// (Pi only passes callbacks to login, not pi).
|
|
348
341
|
const oauthBlock = buildOauthBlock(envBaseUrl, envApiKey, pi);
|
|
349
342
|
|
|
350
|
-
// Initial stub registration so "Rozalia" appears in /login selector
|
|
343
|
+
// Initial stub registration so "Rozalia" appears in /login selector.
|
|
344
|
+
// Credentials are resolved by Pi's OAuth machinery at request time — we no
|
|
345
|
+
// longer read ~/.pi/agent/auth.json directly (see commit history for why).
|
|
351
346
|
pi.registerProvider("rozalia", {
|
|
352
347
|
name: "Rozalia",
|
|
353
348
|
baseUrl: envBaseUrl,
|
|
354
349
|
api: "openai-completions",
|
|
350
|
+
authHeader: true,
|
|
355
351
|
models: [],
|
|
356
352
|
oauth: oauthBlock,
|
|
357
353
|
});
|
|
358
354
|
|
|
359
|
-
// Best-effort: if
|
|
360
|
-
//
|
|
361
|
-
let credsToUse: CredsPayload | null = null;
|
|
355
|
+
// Best-effort: if the API key is provided via env var, register eagerly so
|
|
356
|
+
// models appear without waiting for /login.
|
|
362
357
|
if (envApiKey) {
|
|
363
|
-
credsToUse = { baseUrl: envBaseUrl, apiKey: envApiKey };
|
|
364
|
-
} else if (storedCreds?.apiKey) {
|
|
365
|
-
credsToUse = storedCreds;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (credsToUse) {
|
|
369
358
|
try {
|
|
370
|
-
await registerRozaliaProvider(pi,
|
|
359
|
+
await registerRozaliaProvider(pi, { baseUrl: envBaseUrl, apiKey: envApiKey }, oauthBlock);
|
|
371
360
|
} catch {
|
|
372
361
|
// ignore — will retry on next call
|
|
373
362
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-rozalia",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "OpenAI-compatible provider extension for the Pi coding agent — connects to Rozalia AI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -19,5 +19,8 @@
|
|
|
19
19
|
"extensions": [
|
|
20
20
|
"./extensions"
|
|
21
21
|
]
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
22
25
|
}
|
|
23
26
|
}
|
|
Binary file
|