pi-umans-provider 1.0.35 → 1.0.37
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/.github/workflows/update-models.yml +58 -0
- package/CHANGELOG.md +8 -0
- package/index.ts +20 -5
- package/package.json +7 -3
- package/scripts/test-pi-compat.mjs +2 -2
- package/tests/quiet-startup-sync.test.ts +27 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Update models
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "7 0 * * *" # 07:07 Asia/Ho_Chi_Minh
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
# Publishing uses npm Trusted Publishing (OIDC) bound to this workflow file, so
|
|
9
|
+
# no npm token is stored. A version pushed while publishing was unavailable is
|
|
10
|
+
# published by the next run that finds it missing from npm.
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: update-models
|
|
17
|
+
cancel-in-progress: false
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
update:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
timeout-minutes: 15
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v5
|
|
25
|
+
|
|
26
|
+
- uses: actions/setup-node@v5
|
|
27
|
+
with:
|
|
28
|
+
node-version: 24
|
|
29
|
+
|
|
30
|
+
- name: Fetch the model catalog
|
|
31
|
+
run: node scripts/update-models.js
|
|
32
|
+
|
|
33
|
+
- name: Commit, bump and push
|
|
34
|
+
run: |
|
|
35
|
+
if git diff --quiet; then
|
|
36
|
+
echo "No model changes."
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
git config user.name "github-actions[bot]"
|
|
40
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
41
|
+
git add -A
|
|
42
|
+
git commit -m "feat: update umans models from API"
|
|
43
|
+
npm version patch --no-git-tag-version
|
|
44
|
+
version=$(node -p "require('./package.json').version")
|
|
45
|
+
git commit -am "chore(release): v$version"
|
|
46
|
+
git push origin HEAD:main
|
|
47
|
+
|
|
48
|
+
- name: Publish to npm if this version is missing
|
|
49
|
+
run: |
|
|
50
|
+
name=$(node -p "require('./package.json').name")
|
|
51
|
+
version=$(node -p "require('./package.json').version")
|
|
52
|
+
if npm view "$name@$version" version >/dev/null 2>&1; then
|
|
53
|
+
echo "$name@$version is already on npm."
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
# Trusted publishing needs npm 11.5.1 or newer.
|
|
57
|
+
npm install --global npm@latest
|
|
58
|
+
npm publish --access public --provenance
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.37
|
|
4
|
+
|
|
5
|
+
- Pin Pi SDK development dependencies to 1.2.0 while retaining wildcard host peers; verify real manifest loading, lifecycle and native/bundled Pi hosts offline.
|
|
6
|
+
|
|
7
|
+
## 1.0.36
|
|
8
|
+
|
|
9
|
+
- `/login` while the session-start catalog revalidation is still in flight now fetches the new key's catalog right away: the forced refresh aborts the old-key revalidation and waits for it to release the sync lock instead of skipping as "locked" until the next session start.
|
|
10
|
+
|
|
3
11
|
## 1.0.35
|
|
4
12
|
|
|
5
13
|
- Quiet startup sync: background `/v1/models/info` revalidation runs only with an API key, only when the disk cache is older than 1 hour (`PI_PROVIDER_SYNC_TTL_MS`, `0` = always), missing, or fetched with a different key, and only in one pi process at a time (lock file next to the cache). Failed refreshes back off for 5 minutes; sessions that skip adopt a cache refreshed by another process. `/login` forces a revalidation with the new key.
|
package/index.ts
CHANGED
|
@@ -428,6 +428,9 @@ async function revalidateModels(
|
|
|
428
428
|
|
|
429
429
|
let cachedApiKey: string | undefined;
|
|
430
430
|
let revalidateAbort: AbortController | null = null;
|
|
431
|
+
// The in-flight catalog revalidation (session start or /login); it never rejects.
|
|
432
|
+
let modelSync: Promise<void> = Promise.resolve();
|
|
433
|
+
let modelSyncAbort: AbortController | null = null;
|
|
431
434
|
// Aborted on session replacement/shutdown so in-flight usage fetches from
|
|
432
435
|
// timers don't resolve against a stale ctx (the timer handle itself can't be
|
|
433
436
|
// clearTimeout'd once the async callback has already fired).
|
|
@@ -675,6 +678,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
675
678
|
});
|
|
676
679
|
}
|
|
677
680
|
|
|
681
|
+
// The catalog sync's own abort scope inside the session's: a /login cancels
|
|
682
|
+
// an in-flight (old-key) sync without cancelling the session's usage fetch.
|
|
683
|
+
function supersedeModelSync(session: AbortSignal): AbortSignal {
|
|
684
|
+
modelSyncAbort?.abort();
|
|
685
|
+
modelSyncAbort = new AbortController();
|
|
686
|
+
return AbortSignal.any([session, modelSyncAbort.signal]);
|
|
687
|
+
}
|
|
688
|
+
|
|
678
689
|
// After /login: revalidate with the new key right away (forced past the TTL,
|
|
679
690
|
// still single-flight). Deferred so pi stores the credential first; never
|
|
680
691
|
// awaited, never throws into the login flow.
|
|
@@ -684,8 +695,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
684
695
|
const key = credentials.access;
|
|
685
696
|
cachedApiKey = key;
|
|
686
697
|
if (!revalidateAbort || revalidateAbort.signal.aborted) revalidateAbort = new AbortController();
|
|
687
|
-
|
|
688
|
-
|
|
698
|
+
// The new key supersedes an in-flight revalidation, which must release its
|
|
699
|
+
// sync lock first, or this forced refresh would skip as "locked".
|
|
700
|
+
const signal = supersedeModelSync(revalidateAbort.signal);
|
|
701
|
+
modelSync = modelSync
|
|
702
|
+
.then(() => (signal.aborted ? null : revalidateModels(key, embeddedModels, signal, true)))
|
|
689
703
|
.then((freshBase) => {
|
|
690
704
|
if (freshBase && !signal.aborted) registerFresh(freshBase);
|
|
691
705
|
})
|
|
@@ -777,10 +791,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
777
791
|
const signal = revalidateAbort.signal;
|
|
778
792
|
const keyResolved = resolveApiKey(ctx.modelRegistry);
|
|
779
793
|
|
|
780
|
-
|
|
781
|
-
|
|
794
|
+
const syncSignal = supersedeModelSync(signal);
|
|
795
|
+
modelSync = keyResolved
|
|
796
|
+
.then(() => revalidateModels(cachedApiKey, embeddedModels, syncSignal))
|
|
782
797
|
.then((freshBase) => {
|
|
783
|
-
if (freshBase && !
|
|
798
|
+
if (freshBase && !syncSignal.aborted) registerFresh(freshBase);
|
|
784
799
|
})
|
|
785
800
|
.catch(() => { /* keep serving the stale catalog */ });
|
|
786
801
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-umans-provider",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.37",
|
|
4
4
|
"description": "UMANS provider extension for pi - Access Kimi K2.6, GLM 5.1, Qwen 3.6, and coding-optimized models through the UMANS API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"brace-expansion": "5.0.12"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@earendil-works/pi-ai": "1.
|
|
41
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
40
|
+
"@earendil-works/pi-ai": "1.2.0",
|
|
41
|
+
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
42
42
|
"@types/node": "^25.9.2",
|
|
43
43
|
"typescript": "^6.0.3",
|
|
44
44
|
"vitest": "4.1.7"
|
|
@@ -46,5 +46,9 @@
|
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@earendil-works/pi-ai": "*",
|
|
48
48
|
"@earendil-works/pi-coding-agent": "*"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/monotykamary/pi-umans-provider.git"
|
|
49
53
|
}
|
|
50
54
|
}
|
|
@@ -14,7 +14,7 @@ globalThis.fetch = async () => new Response('', { status: 503 });
|
|
|
14
14
|
let session;
|
|
15
15
|
try {
|
|
16
16
|
const { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, VERSION } = await import('@earendil-works/pi-coding-agent');
|
|
17
|
-
assert.equal(VERSION, '1.
|
|
17
|
+
assert.equal(VERSION, '1.2.0', 'run compatibility checks against the pinned host');
|
|
18
18
|
const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
19
19
|
const hostPackages = ['@earendil-works/pi-ai', '@earendil-works/pi-agent-core', '@earendil-works/pi-coding-agent', '@earendil-works/pi-tui', 'typebox'];
|
|
20
20
|
for (const name of hostPackages) {
|
|
@@ -65,7 +65,7 @@ try {
|
|
|
65
65
|
modelCount += models.length;
|
|
66
66
|
}
|
|
67
67
|
await session.extensionRunner.emit({ type: 'session_shutdown' });
|
|
68
|
-
assert.deepEqual(errors, [], 'startup and shutdown handlers must work with Pi 1.
|
|
68
|
+
assert.deepEqual(errors, [], 'startup and shutdown handlers must work with Pi 1.2.0 contexts');
|
|
69
69
|
console.log(`${manifest.name}: Pi ${VERSION} loader, manifest, session startup, and ${modelCount} models OK`);
|
|
70
70
|
} finally {
|
|
71
71
|
session?.dispose();
|
|
@@ -65,7 +65,7 @@ function writeCache(ageMs: number, models: unknown[] = embeddedModels, fingerpri
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function stubFetch(impl: (url?: unknown) => Promise<Response> = async (url) => (isUsageUrl(url) ? usageResponse() : liveModelsResponse())) {
|
|
68
|
+
function stubFetch(impl: (url?: unknown, init?: RequestInit) => Promise<Response> = async (url) => (isUsageUrl(url) ? usageResponse() : liveModelsResponse())) {
|
|
69
69
|
const fetch = vi.fn(impl);
|
|
70
70
|
vi.stubGlobal("fetch", fetch);
|
|
71
71
|
return fetch;
|
|
@@ -299,6 +299,32 @@ describe("umans: /login and usage polling", () => {
|
|
|
299
299
|
expect(JSON.parse(fs.readFileSync(syncMetaPath(cachePath), "utf8")).keyFingerprint).toBe(keyFingerprint("sk-after-login"));
|
|
300
300
|
});
|
|
301
301
|
|
|
302
|
+
it("fetches the new key's catalog when /login races the session revalidation holding the lock", async () => {
|
|
303
|
+
setupHome();
|
|
304
|
+
writeCache(2 * HOUR);
|
|
305
|
+
const pending: Array<() => void> = [];
|
|
306
|
+
const fetch = stubFetch((url, init?: RequestInit) => new Promise<Response>((resolve, reject) => {
|
|
307
|
+
const signal = init?.signal;
|
|
308
|
+
signal?.addEventListener("abort", () => reject(signal.reason));
|
|
309
|
+
pending.push(() => resolve(isUsageUrl(url) ? usageResponse() : liveModelsResponse()));
|
|
310
|
+
}));
|
|
311
|
+
const ext = await loadExtension();
|
|
312
|
+
await ext.startSession(interactive);
|
|
313
|
+
await vi.waitFor(() => expect(modelCalls(fetch)).toBe(1)); // session sync in flight, lock held
|
|
314
|
+
await vi.waitFor(() => expect(usageCalls(fetch)).toBe(1));
|
|
315
|
+
const login = (ext.registrations[0] as any).oauth.login;
|
|
316
|
+
await login({ onPrompt: async () => "sk-new-login-key" });
|
|
317
|
+
await vi.waitFor(() => expect(modelCalls(fetch)).toBe(2));
|
|
318
|
+
for (const release of pending) release();
|
|
319
|
+
await vi.waitFor(() => expect(hasLiveModel(ext.registrations.at(-1))).toBe(true));
|
|
320
|
+
// Superseding the catalog sync leaves the session's usage footer fetch alone.
|
|
321
|
+
await vi.waitFor(() => expect(ext.setStatus).toHaveBeenCalledWith("umans-usage", expect.stringContaining("Code Max")));
|
|
322
|
+
const modelRequests = fetch.mock.calls.filter(([url]) => !isUsageUrl(url)) as any[][];
|
|
323
|
+
expect(modelRequests[1][1].headers.Authorization).toBe("Bearer sk-new-login-key");
|
|
324
|
+
expect(JSON.parse(fs.readFileSync(syncMetaPath(cachePath), "utf8")).keyFingerprint).toBe(keyFingerprint("sk-new-login-key"));
|
|
325
|
+
expect(fs.existsSync(syncLockPath(cachePath))).toBe(false);
|
|
326
|
+
});
|
|
327
|
+
|
|
302
328
|
it("does not poll usage without a UI (print mode, child agents)", async () => {
|
|
303
329
|
setupHome();
|
|
304
330
|
writeCache(10 * 60 * 1000);
|