pi-umans-provider 1.0.34 → 1.0.36
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/AGENTS.md +1 -0
- package/CHANGELOG.md +12 -0
- package/README.md +19 -0
- package/index.ts +174 -31
- package/package.json +13 -11
- package/sync-policy.ts +309 -0
- package/tests/quiet-startup-sync.test.ts +383 -0
- package/tests/sync-policy.test.ts +259 -0
- package/vitest.config.ts +10 -0
- package/bun.lock +0 -325
package/AGENTS.md
CHANGED
|
@@ -19,6 +19,7 @@ When a model needs overrides, new properties, or corrections, edit the appropria
|
|
|
19
19
|
| `patch.json` | Per-model overrides keyed by model ID. Add reasoning flags, compat settings, pricing corrections, thinking level maps, etc. Applied on top of `models.json` at runtime and for README generation. |
|
|
20
20
|
| `custom-models.json` | Models that don't exist in the provider API (hidden models, router endpoints, cross-provider aliases). Merged after patch. Format: array of full model objects (same schema as `models.json` entries). |
|
|
21
21
|
| `index.ts` | Provider extension code. |
|
|
22
|
+
| `sync-policy.ts` | Quiet startup sync policy (TTL, key fingerprint, cross-process lock). Provider-agnostic and shared verbatim across pi-*-provider repos — keep it free of provider specifics. |
|
|
22
23
|
| `scripts/update-models.js` | The sync script itself (edit only if changing how models are fetched/transformed). |
|
|
23
24
|
|
|
24
25
|
## Data Flow
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.36
|
|
4
|
+
|
|
5
|
+
- `/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.
|
|
6
|
+
|
|
7
|
+
## 1.0.35
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
- Never store the API key: key changes are detected via a salted sha256 fingerprint in `cache/umans-models.sync.json`. The cache file format is unchanged.
|
|
11
|
+
- Usage footer: `/v1/usage` is no longer called without a UI (print mode, headless child agents). The session-start usage fetch is shared across pi processes for 60 seconds via `cache/umans-usage.json`; turn-end and idle polling are unchanged.
|
|
12
|
+
- Write the model cache atomically and swallow API key resolution failures in `session_start`.
|
|
13
|
+
- Add vitest coverage for the sync policy and the session_start integration.
|
|
14
|
+
|
|
3
15
|
## 1.0.32
|
|
4
16
|
|
|
5
17
|
- Validate provider registration and session lifecycle against Pi 1.0.0; update the pinned development SDK.
|
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ _A [pi](https://github.com/earendil-works/pi-coding-agent) provider extension wi
|
|
|
22
22
|
- **Streaming** - Real-time token streaming
|
|
23
23
|
- **Subscription-based** - All models included in your plan, no per-token cost
|
|
24
24
|
- **Usage status bar** — Displays your plan, concurrent sessions, and remaining requests in the pi footer
|
|
25
|
+
- **Quiet live model sync** — Models refresh from the UMANS API in the background, at most hourly and by one pi process at a time ([details](#model-sync))
|
|
25
26
|
|
|
26
27
|
## Available Models
|
|
27
28
|
|
|
@@ -105,6 +106,7 @@ Get your API key from [code.umans.ai](https://code.umans.ai).
|
|
|
105
106
|
| Variable | Required | Description |
|
|
106
107
|
|----------|----------|-------------|
|
|
107
108
|
| `UMANS_API_KEY` | No | Your UMANS API key (fallback if not in auth.json) |
|
|
109
|
+
| `PI_PROVIDER_SYNC_TTL_MS` | No | Model sync freshness window in ms (default `3600000`; `0` = revalidate every session start) |
|
|
108
110
|
|
|
109
111
|
## Configuration
|
|
110
112
|
|
|
@@ -158,6 +160,23 @@ Code Max (Founding Seat) | ⟠ 1/4
|
|
|
158
160
|
|
|
159
161
|
The concurrent-session count is tracked locally: it's optimistically incremented the moment an agent run starts (no API call needed — the server has a brief registration lag anyway), reconciled with UMANS's `/v1/usage` endpoint after each agent run ends, and lightly polled while idle so the baseline stays fresh. The plan name and request budget are fetched on session start and model selection.
|
|
160
162
|
|
|
163
|
+
The footer (and every `/v1/usage` call) is interactive-only: print mode (`pi -p`) and headless child agents never poll. On session start, one pi process per 60 seconds calls `/v1/usage` and writes the result to `~/.pi/agent/cache/umans-usage.json` (with the same lock and key fingerprint as the model sync); other sessions started within that window render it from disk.
|
|
164
|
+
|
|
165
|
+
## Model sync
|
|
166
|
+
|
|
167
|
+
Startup never waits on the network. pi starts with the cached catalog (`~/.pi/agent/cache/umans-models.json`,
|
|
168
|
+
falling back to the bundled `models.json`) and revalidates against `/v1/models/info` in the background — only when:
|
|
169
|
+
|
|
170
|
+
- a UMANS API key is configured (no key, no request),
|
|
171
|
+
- the cache is older than 1 hour, missing, or was fetched with a different API key, and
|
|
172
|
+
- no other pi process is already refreshing it (an `umans-models.lock` file next to the cache).
|
|
173
|
+
|
|
174
|
+
So `/new`, `/reload`, `pi -p` and fan-out child agents share one cache instead of each calling the API; a
|
|
175
|
+
session that skips the fetch still picks up a cache another process refreshed. Logging in with `/login` revalidates
|
|
176
|
+
right away. A failed refresh is retried after 5 minutes at the earliest. `PI_PROVIDER_SYNC_TTL_MS` changes the
|
|
177
|
+
window (`0` revalidates on every session start). Only a hash fingerprint of the key is stored
|
|
178
|
+
(`umans-models.sync.json`), never the key itself.
|
|
179
|
+
|
|
161
180
|
## API Compatibility
|
|
162
181
|
|
|
163
182
|
The UMANS API follows OpenAI conventions with these differences (handled via `patch.json` and `before_provider_request` hook):
|
package/index.ts
CHANGED
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* Model resolution strategy: Stale-While-Revalidate
|
|
20
20
|
* 1. Serve stale immediately: disk cache → embedded models.json (zero-latency)
|
|
21
21
|
* 2. Revalidate in background: live API /v1/models/info → merge with embedded → cache → hot-swap
|
|
22
|
+
* Quiet startup sync (sync-policy.ts): only with an API key, only when the
|
|
23
|
+
* disk cache is older than the TTL (1h, PI_PROVIDER_SYNC_TTL_MS) or the key
|
|
24
|
+
* changed, and only one pi process at a time (lock file next to the cache).
|
|
25
|
+
* /login forces a revalidation with the new key.
|
|
22
26
|
* 3. patch.json + custom-models.json applied on top of whichever source won
|
|
23
27
|
*
|
|
24
28
|
* Merge order: [live|cache|embedded] → apply patch.json → merge custom-models.json
|
|
@@ -45,6 +49,14 @@ import patchData from "./patch.json" with { type: "json" };
|
|
|
45
49
|
import deprecatedData from "./deprecated-models.json" with { type: "json" };
|
|
46
50
|
import fs from "fs";
|
|
47
51
|
import path from "path";
|
|
52
|
+
import {
|
|
53
|
+
acquireModelSync,
|
|
54
|
+
cacheChangedSinceLoad,
|
|
55
|
+
keyFingerprint,
|
|
56
|
+
markCacheLoaded,
|
|
57
|
+
syncMetaPath,
|
|
58
|
+
writeFileAtomic,
|
|
59
|
+
} from "./sync-policy";
|
|
48
60
|
|
|
49
61
|
// ─── Usage/Plan Types ────────────────────────────────────────────────────────
|
|
50
62
|
|
|
@@ -53,6 +65,15 @@ const USAGE_FETCH_TIMEOUT_MS = 5000;
|
|
|
53
65
|
const USAGE_THROTTLE_MS = 30_000;
|
|
54
66
|
const END_SETTLE_MS = 2000;
|
|
55
67
|
const IDLE_POLL_MS = 45_000;
|
|
68
|
+
/**
|
|
69
|
+
* Startup usage dedupe window. Every interactive session_start used to call
|
|
70
|
+
* /v1/usage; now one process per window fetches and writes the result to
|
|
71
|
+
* `cache/umans-usage.json`, and sessions started inside the window (/new,
|
|
72
|
+
* /reload, a second terminal) render it from disk. 60s: the plan name and
|
|
73
|
+
* request budget barely move, and the concurrent-session baseline is refreshed
|
|
74
|
+
* by the idle poll (IDLE_POLL_MS) and after every agent run anyway.
|
|
75
|
+
*/
|
|
76
|
+
const USAGE_SYNC_TTL_MS = 60_000;
|
|
56
77
|
|
|
57
78
|
let sessionPlan: string | null = null;
|
|
58
79
|
let sessionConcurrency: number | null = null;
|
|
@@ -287,7 +308,8 @@ function loadCachedModels(): JsonModel[] | null {
|
|
|
287
308
|
function cacheModels(models: JsonModel[]): void {
|
|
288
309
|
try {
|
|
289
310
|
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
290
|
-
|
|
311
|
+
// Atomic (tmp + rename): other pi processes may be reading the cache right now.
|
|
312
|
+
writeFileAtomic(CACHE_PATH, JSON.stringify(models, null, 2) + "\n");
|
|
291
313
|
} catch {
|
|
292
314
|
// Cache write failure is non-fatal
|
|
293
315
|
}
|
|
@@ -359,6 +381,7 @@ function withDeprecated(models: JsonModel[]): JsonModel[] {
|
|
|
359
381
|
}
|
|
360
382
|
|
|
361
383
|
function loadStaleModels(embeddedModels: JsonModel[]): JsonModel[] {
|
|
384
|
+
markCacheLoaded(CACHE_PATH); // stat before read: lets session_start spot newer caches
|
|
362
385
|
const cached = loadCachedModels();
|
|
363
386
|
if (!cached || cached.length === 0) return embeddedModels;
|
|
364
387
|
|
|
@@ -371,23 +394,43 @@ function loadStaleModels(embeddedModels: JsonModel[]): JsonModel[] {
|
|
|
371
394
|
return cached;
|
|
372
395
|
}
|
|
373
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Background revalidation, gated by the quiet startup sync policy
|
|
399
|
+
* (sync-policy.ts): no key → no fetch; fresh cache (TTL) → no fetch; another
|
|
400
|
+
* process already fetching → no fetch. When skipped, a cache that another
|
|
401
|
+
* process refreshed since we loaded it is adopted from disk instead.
|
|
402
|
+
* `force` bypasses the TTL (e.g. after /login); it is still single-flight.
|
|
403
|
+
*
|
|
404
|
+
* The Umans "OAuth" credential is the API key itself (access === refresh, never
|
|
405
|
+
* rotated), so it doubles as the stable identity for the key fingerprint.
|
|
406
|
+
*/
|
|
374
407
|
async function revalidateModels(
|
|
375
408
|
apiKey: string | undefined,
|
|
376
409
|
embeddedModels: JsonModel[],
|
|
377
410
|
signal?: AbortSignal,
|
|
411
|
+
force = false,
|
|
378
412
|
): Promise<JsonModel[] | null> {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
413
|
+
const lease = acquireModelSync({ cachePath: CACHE_PATH, apiKey, force });
|
|
414
|
+
if (!lease) return cacheChangedSinceLoad(CACHE_PATH) ? loadStaleModels(embeddedModels) : null;
|
|
415
|
+
let merged: JsonModel[] | null = null;
|
|
416
|
+
try {
|
|
417
|
+
const liveModels = apiKey ? await fetchLiveModels(apiKey, signal) : null;
|
|
418
|
+
if (!liveModels || liveModels.length === 0) return null;
|
|
419
|
+
merged = mergeWithEmbedded(liveModels, embeddedModels);
|
|
420
|
+
cacheModels(merged);
|
|
421
|
+
return merged;
|
|
422
|
+
} finally {
|
|
423
|
+
lease.release(merged ? "fetched" : signal?.aborted ? "aborted" : "failed");
|
|
424
|
+
}
|
|
385
425
|
}
|
|
386
426
|
|
|
387
427
|
// ─── API Key Resolution ────────────────────────────────────────────────────────
|
|
388
428
|
|
|
389
429
|
let cachedApiKey: string | undefined;
|
|
390
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;
|
|
391
434
|
// Aborted on session replacement/shutdown so in-flight usage fetches from
|
|
392
435
|
// timers don't resolve against a stale ctx (the timer handle itself can't be
|
|
393
436
|
// clearTimeout'd once the async callback has already fired).
|
|
@@ -445,6 +488,59 @@ async function fetchUsage(
|
|
|
445
488
|
}
|
|
446
489
|
}
|
|
447
490
|
|
|
491
|
+
// ─── Shared usage snapshot (cross-process startup dedupe) ─────────────────────
|
|
492
|
+
|
|
493
|
+
const USAGE_CACHE_PATH = path.join(CACHE_DIR, `${PROVIDER_ID}-usage.json`);
|
|
494
|
+
|
|
495
|
+
/** Read the usage snapshot another process wrote — only if it was fetched with this key. */
|
|
496
|
+
function readUsageCache(apiKey: string): UmansUsage | null {
|
|
497
|
+
try {
|
|
498
|
+
const meta = JSON.parse(fs.readFileSync(syncMetaPath(USAGE_CACHE_PATH), "utf8"));
|
|
499
|
+
if (meta?.keyFingerprint !== keyFingerprint(apiKey)) return null;
|
|
500
|
+
const data = JSON.parse(fs.readFileSync(USAGE_CACHE_PATH, "utf8")) as UmansUsage;
|
|
501
|
+
return data?.plan?.slug ? data : null;
|
|
502
|
+
} catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function writeUsageCache(usage: UmansUsage): void {
|
|
508
|
+
try {
|
|
509
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
510
|
+
writeFileAtomic(USAGE_CACHE_PATH, JSON.stringify(usage) + "\n");
|
|
511
|
+
} catch {
|
|
512
|
+
// Non-fatal: other processes just fetch for themselves next time.
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Usage for session_start: one pi process per USAGE_SYNC_TTL_MS window calls
|
|
518
|
+
* /v1/usage (same lease/lock/backoff policy as the model sync, own cache
|
|
519
|
+
* file); the others render the snapshot it wrote. Never fetches without a key.
|
|
520
|
+
* `fetched` is false when this process skipped the network.
|
|
521
|
+
*/
|
|
522
|
+
async function startupUsage(
|
|
523
|
+
apiKey: string | undefined,
|
|
524
|
+
signal: AbortSignal,
|
|
525
|
+
): Promise<{ usage: UmansUsage | null; fetched: boolean }> {
|
|
526
|
+
if (!apiKey) return { usage: null, fetched: false };
|
|
527
|
+
const lease = acquireModelSync({
|
|
528
|
+
cachePath: USAGE_CACHE_PATH,
|
|
529
|
+
apiKey,
|
|
530
|
+
ttlMs: USAGE_SYNC_TTL_MS,
|
|
531
|
+
isValidCache: (data) => data != null,
|
|
532
|
+
});
|
|
533
|
+
if (!lease) return { usage: readUsageCache(apiKey), fetched: false };
|
|
534
|
+
let usage: UmansUsage | null = null;
|
|
535
|
+
try {
|
|
536
|
+
usage = await throttledFetchUsage(apiKey, { force: true, signal });
|
|
537
|
+
if (usage) writeUsageCache(usage);
|
|
538
|
+
return { usage, fetched: true };
|
|
539
|
+
} finally {
|
|
540
|
+
lease.release(usage ? "fetched" : signal.aborted ? "aborted" : "failed");
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
448
544
|
function applyUsage(usage: UmansUsage, ctx: any): void {
|
|
449
545
|
sessionPlan = usage.plan.display_name;
|
|
450
546
|
sessionConcurrency = usage.limits?.concurrency?.limit ?? null;
|
|
@@ -573,6 +669,45 @@ export default function (pi: ExtensionAPI) {
|
|
|
573
669
|
const staleBase = loadStaleModels(embeddedModels);
|
|
574
670
|
const staleModels = buildModels(staleBase, customModels, patches);
|
|
575
671
|
|
|
672
|
+
function registerFresh(freshBase: JsonModel[]): void {
|
|
673
|
+
pi.registerProvider("umans", {
|
|
674
|
+
baseUrl: BASE_URL,
|
|
675
|
+
apiKey: "$UMANS_API_KEY",
|
|
676
|
+
api: "openai-completions",
|
|
677
|
+
models: buildModels(freshBase, customModels, patches),
|
|
678
|
+
});
|
|
679
|
+
}
|
|
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
|
+
|
|
689
|
+
// After /login: revalidate with the new key right away (forced past the TTL,
|
|
690
|
+
// still single-flight). Deferred so pi stores the credential first; never
|
|
691
|
+
// awaited, never throws into the login flow.
|
|
692
|
+
async function loginAndRevalidate(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
693
|
+
const credentials = await loginUmans(callbacks);
|
|
694
|
+
setTimeout(() => {
|
|
695
|
+
const key = credentials.access;
|
|
696
|
+
cachedApiKey = key;
|
|
697
|
+
if (!revalidateAbort || revalidateAbort.signal.aborted) revalidateAbort = new AbortController();
|
|
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)))
|
|
703
|
+
.then((freshBase) => {
|
|
704
|
+
if (freshBase && !signal.aborted) registerFresh(freshBase);
|
|
705
|
+
})
|
|
706
|
+
.catch(() => { /* keep serving the current catalog */ });
|
|
707
|
+
}, 0);
|
|
708
|
+
return credentials;
|
|
709
|
+
}
|
|
710
|
+
|
|
576
711
|
pi.registerProvider("umans", {
|
|
577
712
|
baseUrl: BASE_URL,
|
|
578
713
|
apiKey: "$UMANS_API_KEY",
|
|
@@ -580,7 +715,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
580
715
|
models: staleModels,
|
|
581
716
|
oauth: {
|
|
582
717
|
name: "Umans AI (API Key)",
|
|
583
|
-
login:
|
|
718
|
+
login: loginAndRevalidate,
|
|
584
719
|
refreshToken: refreshUmansToken,
|
|
585
720
|
getApiKey: getApiKey,
|
|
586
721
|
},
|
|
@@ -644,6 +779,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
644
779
|
return p;
|
|
645
780
|
});
|
|
646
781
|
|
|
782
|
+
// Fire-and-forget: never await network here — startup must not block on the
|
|
783
|
+
// provider API. Nothing is awaited and every rejection is swallowed.
|
|
647
784
|
pi.on("session_start", async (_event, ctx) => {
|
|
648
785
|
revalidateAbort?.abort();
|
|
649
786
|
revalidateAbort = new AbortController();
|
|
@@ -652,32 +789,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
652
789
|
usageAbort?.abort();
|
|
653
790
|
usageAbort = new AbortController();
|
|
654
791
|
const signal = revalidateAbort.signal;
|
|
655
|
-
resolveApiKey(ctx.modelRegistry)
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
792
|
+
const keyResolved = resolveApiKey(ctx.modelRegistry);
|
|
793
|
+
|
|
794
|
+
const syncSignal = supersedeModelSync(signal);
|
|
795
|
+
modelSync = keyResolved
|
|
796
|
+
.then(() => revalidateModels(cachedApiKey, embeddedModels, syncSignal))
|
|
797
|
+
.then((freshBase) => {
|
|
798
|
+
if (freshBase && !syncSignal.aborted) registerFresh(freshBase);
|
|
799
|
+
})
|
|
800
|
+
.catch(() => { /* keep serving the stale catalog */ });
|
|
801
|
+
|
|
802
|
+
// Usage footer: interactive sessions only (no footer in print mode or
|
|
803
|
+
// headless child agents, so no /v1/usage call there either).
|
|
804
|
+
void keyResolved
|
|
805
|
+
.then(async () => {
|
|
806
|
+
if (!isUmansModel(ctx)) {
|
|
807
|
+
clearUsageStatus(ctx);
|
|
808
|
+
return;
|
|
664
809
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
applyUsage(usage, ctx);
|
|
675
|
-
}
|
|
676
|
-
});
|
|
810
|
+
if (!ctx.hasUI) return;
|
|
811
|
+
const { usage, fetched } = await startupUsage(cachedApiKey, signal);
|
|
812
|
+
if (signal.aborted) return;
|
|
813
|
+
if (usage) applyUsage(usage, ctx);
|
|
814
|
+
// Skipped the network and nothing usable on disk yet (another process
|
|
815
|
+
// is fetching right now): let the idle poll fill the footer in.
|
|
816
|
+
else if (!fetched && cachedApiKey) resetIdleTimer(ctx);
|
|
817
|
+
})
|
|
818
|
+
.catch(() => { /* the footer stays empty */ });
|
|
677
819
|
});
|
|
678
820
|
|
|
679
821
|
pi.on("model_select", (event, ctx) => {
|
|
680
822
|
if (event.model?.provider === "umans") {
|
|
823
|
+
if (!ctx.hasUI) return; // no footer to render without a UI
|
|
681
824
|
throttledFetchUsage(cachedApiKey, { force: true }).then((usage) => {
|
|
682
825
|
if (usage) {
|
|
683
826
|
applyUsage(usage, ctx);
|
|
@@ -695,7 +838,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
695
838
|
// registration lag, so fetching now would undercount us anyway. One span
|
|
696
839
|
// per prompt (agent_start → agent_settled), so no flicker between tool turns.
|
|
697
840
|
pi.on("agent_start", async (_event, ctx) => {
|
|
698
|
-
if (!isUmansModel(ctx)) return;
|
|
841
|
+
if (!isUmansModel(ctx) || !ctx.hasUI) return;
|
|
699
842
|
if (idleTimer) {
|
|
700
843
|
clearTimeout(idleTimer);
|
|
701
844
|
idleTimer = null;
|
|
@@ -707,7 +850,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
707
850
|
// Turn ended: drop our optimistic +1 and reconcile with the server after a
|
|
708
851
|
// short settle so the server has dropped our session from its count.
|
|
709
852
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
710
|
-
if (!isUmansModel(ctx)) return;
|
|
853
|
+
if (!isUmansModel(ctx) || !ctx.hasUI) return;
|
|
711
854
|
activeStreams = Math.max(0, activeStreams - 1);
|
|
712
855
|
updateUsageStatus(ctx);
|
|
713
856
|
scheduleEndFetch(ctx);
|
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-umans-provider",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.36",
|
|
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",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"clean": "echo 'nothing to clean'",
|
|
9
|
+
"build": "echo 'nothing to build'",
|
|
10
|
+
"check": "tsc --noEmit && vitest run && node scripts/test-pi-compat.mjs",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"update-models": "node scripts/update-models.js",
|
|
13
|
+
"test:pi": "node scripts/test-pi-compat.mjs",
|
|
14
|
+
"typecheck": "tsc --noEmit"
|
|
15
|
+
},
|
|
7
16
|
"keywords": [
|
|
8
17
|
"pi",
|
|
9
18
|
"extension",
|
|
@@ -31,18 +40,11 @@
|
|
|
31
40
|
"@earendil-works/pi-ai": "1.1.0",
|
|
32
41
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
33
42
|
"@types/node": "^25.9.2",
|
|
34
|
-
"typescript": "^6.0.3"
|
|
43
|
+
"typescript": "^6.0.3",
|
|
44
|
+
"vitest": "4.1.7"
|
|
35
45
|
},
|
|
36
46
|
"peerDependencies": {
|
|
37
47
|
"@earendil-works/pi-ai": "*",
|
|
38
48
|
"@earendil-works/pi-coding-agent": "*"
|
|
39
|
-
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"clean": "echo 'nothing to clean'",
|
|
42
|
-
"build": "echo 'nothing to build'",
|
|
43
|
-
"check": "tsc --noEmit && node scripts/test-pi-compat.mjs",
|
|
44
|
-
"update-models": "node scripts/update-models.js",
|
|
45
|
-
"test:pi": "node scripts/test-pi-compat.mjs",
|
|
46
|
-
"typecheck": "tsc --noEmit"
|
|
47
49
|
}
|
|
48
|
-
}
|
|
50
|
+
}
|