@oh-my-pi/pi-ai 18.7.0 → 18.8.0
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/CHANGELOG.md +22 -0
- package/dist/types/auth/sqlite-credential-store.d.ts +1 -0
- package/dist/types/auth/store.d.ts +6 -0
- package/dist/types/auth-broker/remote-store.d.ts +1 -0
- package/dist/types/auth-broker/snapshot-cache.d.ts +12 -0
- package/dist/types/auth-gateway/session-state.d.ts +6 -0
- package/dist/types/dialect/terminator-wait.d.ts +19 -0
- package/dist/types/env-api-key.d.ts +21 -0
- package/dist/types/images/shared.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/providers/connect-frame.d.ts +44 -0
- package/dist/types/providers/cursor/interaction-query.d.ts +0 -2
- package/dist/types/stream.d.ts +0 -21
- package/dist/types/transcription/types.d.ts +2 -1
- package/dist/types/utils/aws-profile.d.ts +5 -0
- package/package.json +6 -6
- package/src/auth/affinity.ts +33 -24
- package/src/auth/blocks.ts +90 -14
- package/src/auth/cascade.ts +1 -1
- package/src/auth/select.ts +27 -2
- package/src/auth/sqlite-credential-store.ts +85 -42
- package/src/auth/store.ts +6 -0
- package/src/auth/usage-cache.ts +16 -5
- package/src/auth/usage.ts +1 -1
- package/src/auth-broker/discover.ts +5 -4
- package/src/auth-broker/remote-store.ts +117 -46
- package/src/auth-broker/server.ts +283 -123
- package/src/auth-broker/snapshot-cache.ts +142 -2
- package/src/auth-gateway/server.ts +19 -5
- package/src/auth-gateway/session-state.ts +51 -9
- package/src/dialect/deepseek.ts +10 -2
- package/src/dialect/gemini.ts +8 -2
- package/src/dialect/hermes.ts +8 -2
- package/src/dialect/kimi.ts +8 -2
- package/src/dialect/qwen-xml.ts +8 -2
- package/src/dialect/qwen3.ts +8 -2
- package/src/dialect/terminator-wait.ts +42 -0
- package/src/env-api-key.ts +78 -0
- package/src/images/openai-hosted.ts +11 -4
- package/src/images/openai-images.ts +19 -13
- package/src/images/shared.ts +10 -2
- package/src/index.ts +1 -0
- package/src/providers/anthropic.ts +39 -13
- package/src/providers/apple-foundation-models.ts +10 -4
- package/src/providers/azure-openai-responses.ts +1 -1
- package/src/providers/connect-frame.ts +107 -0
- package/src/providers/cursor/interaction-query.ts +1 -9
- package/src/providers/cursor.ts +87 -47
- package/src/providers/devin.ts +32 -41
- package/src/providers/gitlab-duo-workflow.ts +5 -2
- package/src/providers/google.ts +1 -1
- package/src/providers/ollama.ts +1 -1
- package/src/providers/openai-codex-responses.ts +26 -18
- package/src/providers/openai-completions.ts +1 -1
- package/src/providers/openai-responses.ts +5 -4
- package/src/providers/openai-shared.ts +53 -18
- package/src/providers/transcriptions-server.ts +1 -1
- package/src/providers/transform-messages.ts +17 -13
- package/src/registry/aws.ts +51 -14
- package/src/registry/cloudflare-ai-gateway.ts +24 -1
- package/src/stream.ts +3 -72
- package/src/transcription/types.ts +2 -1
- package/src/utils/aws-profile.ts +10 -4
- package/src/utils/thinking-loop.ts +18 -3
- package/src/utils.ts +22 -6
package/src/auth/cascade.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { authPolicyFor } from "@oh-my-pi/pi-catalog/compat/auth";
|
|
2
2
|
import { $env, $envExact } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { type ApiKeyResolver, markAfterSiblingWait, type ResolvedApiKey } from "../auth-retry";
|
|
4
|
+
import { getEnvApiKey, getEnvApiKeyName } from "../env-api-key";
|
|
4
5
|
import * as AIError from "../error";
|
|
5
6
|
import { isUsageLimitOutcome } from "../error/rate-limit";
|
|
6
7
|
import { AUTHENTICATED_SENTINEL } from "../registry/types";
|
|
7
|
-
import { getEnvApiKey, getEnvApiKeyName } from "../stream";
|
|
8
8
|
import type { SessionAffinity } from "./affinity";
|
|
9
9
|
import type { CredentialPool } from "./pool";
|
|
10
10
|
import type { CredentialSelector } from "./select";
|
package/src/auth/select.ts
CHANGED
|
@@ -28,7 +28,13 @@ import {
|
|
|
28
28
|
} from "./rank";
|
|
29
29
|
import { mergeRefreshedCredential, OAUTH_REFRESH_SKEW_MS, type OAuthRefresher } from "./refresh";
|
|
30
30
|
import type { AuthCredentialStore } from "./store";
|
|
31
|
-
import type {
|
|
31
|
+
import type {
|
|
32
|
+
ApiKeyCredential,
|
|
33
|
+
AuthApiKeyOptions,
|
|
34
|
+
AuthCredential,
|
|
35
|
+
OAuthCredential,
|
|
36
|
+
StoredAuthCredential,
|
|
37
|
+
} from "./types";
|
|
32
38
|
import type { UsageService } from "./usage";
|
|
33
39
|
import {
|
|
34
40
|
isUsageLimitReached,
|
|
@@ -690,6 +696,10 @@ export class CredentialSelector {
|
|
|
690
696
|
const forceRefreshIndex = options?.forceRefresh
|
|
691
697
|
? (sessionPreferredIndex ?? candidates[0]?.selection.index)
|
|
692
698
|
: undefined;
|
|
699
|
+
// Each candidate's synchronous prefix below runs back to back inside `map`,
|
|
700
|
+
// before any await, so one provider re-list serves every initial rebind.
|
|
701
|
+
// Resyncs after an await (refresh, disable) re-read the store.
|
|
702
|
+
let preflightRows: StoredAuthCredential[] | undefined;
|
|
693
703
|
await Promise.all(
|
|
694
704
|
candidates.map(async candidate => {
|
|
695
705
|
const force = forceRefreshIndex !== undefined && candidate.selection.index === forceRefreshIndex;
|
|
@@ -697,7 +707,8 @@ export class CredentialSelector {
|
|
|
697
707
|
let syncedPeerCredential = false;
|
|
698
708
|
if (initialCredentialId !== undefined) {
|
|
699
709
|
const beforeSync = candidate.selection.credential;
|
|
700
|
-
|
|
710
|
+
preflightRows ??= this.#reloadProviderRows(provider);
|
|
711
|
+
if (!this.#rebindOAuthSelection(preflightRows, candidate.selection, initialCredentialId)) return;
|
|
701
712
|
syncedPeerCredential = !authCredentialEquals(beforeSync, candidate.selection.credential);
|
|
702
713
|
}
|
|
703
714
|
const hasFreshAccess = Date.now() + OAUTH_REFRESH_SKEW_MS < candidate.selection.credential.expires;
|
|
@@ -881,11 +892,25 @@ export class CredentialSelector {
|
|
|
881
892
|
selection: { credential: OAuthCredential; index: number },
|
|
882
893
|
credentialId: number,
|
|
883
894
|
): boolean {
|
|
895
|
+
return this.#rebindOAuthSelection(this.#reloadProviderRows(provider), selection, credentialId);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/** Re-list one provider's active rows and adopt them in the pool. */
|
|
899
|
+
#reloadProviderRows(provider: string): StoredAuthCredential[] {
|
|
884
900
|
const latestRows = this.#deps.store.listAuthCredentials(provider);
|
|
885
901
|
this.#deps.pool.replace(
|
|
886
902
|
provider,
|
|
887
903
|
latestRows.map(row => ({ id: row.id, credential: row.credential })),
|
|
888
904
|
);
|
|
905
|
+
return latestRows;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** Point `selection` at row `credentialId` in `latestRows`; false when it is gone or no longer OAuth. */
|
|
909
|
+
#rebindOAuthSelection(
|
|
910
|
+
latestRows: readonly StoredAuthCredential[],
|
|
911
|
+
selection: { credential: OAuthCredential; index: number },
|
|
912
|
+
credentialId: number,
|
|
913
|
+
): boolean {
|
|
889
914
|
const latestIndex = latestRows.findIndex(row => row.id === credentialId);
|
|
890
915
|
if (latestIndex === -1) return false;
|
|
891
916
|
const latest = latestRows[latestIndex];
|
|
@@ -110,6 +110,12 @@ const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
|
|
|
110
110
|
const LEGACY_CODEX_BLOCK_PROVIDER_KEY = "openai-codex:oauth";
|
|
111
111
|
const LEGACY_CODEX_BLOCK_SCOPE = "shared";
|
|
112
112
|
const CODEX_METER_BLOCK_SCOPES = ["chat", "spark"] as const;
|
|
113
|
+
/**
|
|
114
|
+
* Minimum spacing between read-path sweeps of expired block rows. Reads already
|
|
115
|
+
* filter `blocked_until_ms > now`; the sweep only bounds table growth, so it
|
|
116
|
+
* must not take the SQLite write lock on every selection.
|
|
117
|
+
*/
|
|
118
|
+
const CREDENTIAL_BLOCK_SWEEP_INTERVAL_MS = 60_000;
|
|
113
119
|
|
|
114
120
|
// SQLite error classifiers live in pi-utils so the credential store and the
|
|
115
121
|
// model cache share one implementation; re-exported here to preserve the
|
|
@@ -386,11 +392,14 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
386
392
|
#updateIfMatchesWithLeaseStmt: Statement;
|
|
387
393
|
#deleteIfMatchesWithLeaseStmt: Statement;
|
|
388
394
|
#getCredentialBlockStmt: Statement;
|
|
389
|
-
#
|
|
395
|
+
#listCredentialBlockScopesStmt: Statement;
|
|
396
|
+
#listCredentialBlocksStmt: Statement;
|
|
390
397
|
#upsertCredentialBlockStmt: Statement;
|
|
391
398
|
#deleteCredentialBlocksStmt: Statement;
|
|
392
399
|
#deleteCredentialBlockStmt: Statement;
|
|
393
400
|
#deleteExpiredCredentialBlocksStmt: Statement;
|
|
401
|
+
/** Wall-clock time of the last expired-block sweep; throttles read-path sweeps. */
|
|
402
|
+
#lastCredentialBlockSweepMs = Number.NEGATIVE_INFINITY;
|
|
394
403
|
#acquireCredentialRefreshLeaseStmt: Statement;
|
|
395
404
|
#getCredentialRefreshLeaseStmt: Statement;
|
|
396
405
|
#renewCredentialRefreshLeaseStmt: Statement;
|
|
@@ -475,12 +484,18 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
475
484
|
this.#getCredentialBlockStmt = this.#db.prepare(
|
|
476
485
|
"SELECT blocked_until_ms, updated_at FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND block_scope = ? AND blocked_until_ms > ?",
|
|
477
486
|
);
|
|
478
|
-
this.#
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
487
|
+
this.#listCredentialBlockScopesStmt = this.#db.prepare(
|
|
488
|
+
"SELECT block_scope, blocked_until_ms FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND blocked_until_ms > ?",
|
|
489
|
+
);
|
|
490
|
+
// One statement for any number of ids: `json_each` keeps the caller's id
|
|
491
|
+
// order (`ids.key`) and the per-credential provider/scope order.
|
|
492
|
+
this.#listCredentialBlocksStmt = this.#db.prepare(
|
|
493
|
+
`SELECT b.credential_id, b.provider_key, b.block_scope, b.blocked_until_ms, b.updated_at
|
|
494
|
+
FROM json_each(?) AS ids
|
|
495
|
+
JOIN auth_credential_blocks AS b ON b.credential_id = ids.value
|
|
496
|
+
WHERE b.blocked_until_ms > ?
|
|
497
|
+
AND NOT (b.provider_key = ? AND b.block_scope = ?)
|
|
498
|
+
ORDER BY ids.key ASC, b.provider_key ASC, b.block_scope ASC`,
|
|
484
499
|
);
|
|
485
500
|
this.#upsertCredentialBlockStmt = this.#db.prepare(
|
|
486
501
|
`INSERT INTO auth_credential_blocks (credential_id, provider_key, block_scope, blocked_until_ms, updated_at)
|
|
@@ -1585,27 +1600,38 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1585
1600
|
}
|
|
1586
1601
|
|
|
1587
1602
|
getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined {
|
|
1588
|
-
const nowMs = Date.now();
|
|
1589
|
-
const isCodexBlock = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
|
|
1590
1603
|
// Current callers use meter scopes. The physical shared row exists only
|
|
1591
1604
|
// for direct SQLite readers from pre-meter releases.
|
|
1592
|
-
if (
|
|
1605
|
+
if (providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
|
|
1593
1606
|
return undefined;
|
|
1594
1607
|
}
|
|
1595
|
-
|
|
1596
|
-
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope,
|
|
1608
|
+
this.#sweepExpiredCredentialBlocks();
|
|
1609
|
+
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, Date.now()) as
|
|
1597
1610
|
| { blocked_until_ms?: number; updated_at?: number }
|
|
1598
1611
|
| undefined;
|
|
1599
1612
|
return typeof row?.blocked_until_ms === "number" ? row.blocked_until_ms : undefined;
|
|
1600
1613
|
}
|
|
1601
1614
|
|
|
1615
|
+
getCredentialBlockScopes(credentialId: number, providerKey: string): Map<string, number> {
|
|
1616
|
+
this.#sweepExpiredCredentialBlocks();
|
|
1617
|
+
const rows = this.#listCredentialBlockScopesStmt.all(credentialId, providerKey, Date.now()) as Array<
|
|
1618
|
+
Pick<CredentialBlockRow, "block_scope" | "blocked_until_ms">
|
|
1619
|
+
>;
|
|
1620
|
+
const hidesLegacyShared = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
|
|
1621
|
+
const scopes = new Map<string, number>();
|
|
1622
|
+
for (const row of rows) {
|
|
1623
|
+
if (hidesLegacyShared && row.block_scope === LEGACY_CODEX_BLOCK_SCOPE) continue;
|
|
1624
|
+
scopes.set(row.block_scope, row.blocked_until_ms);
|
|
1625
|
+
}
|
|
1626
|
+
return scopes;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1602
1629
|
getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined {
|
|
1603
|
-
|
|
1604
|
-
const isCodexBlock = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
|
|
1605
|
-
if (isCodexBlock && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
|
|
1630
|
+
if (providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
|
|
1606
1631
|
return undefined;
|
|
1607
1632
|
}
|
|
1608
|
-
|
|
1633
|
+
this.#sweepExpiredCredentialBlocks();
|
|
1634
|
+
const nowMs = Date.now();
|
|
1609
1635
|
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, nowMs) as
|
|
1610
1636
|
| { blocked_until_ms?: number; updated_at?: number }
|
|
1611
1637
|
| undefined;
|
|
@@ -1622,7 +1648,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1622
1648
|
const isLegacyCodexBlock =
|
|
1623
1649
|
block.providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && block.blockScope === LEGACY_CODEX_BLOCK_SCOPE;
|
|
1624
1650
|
const blockScopes = isLegacyCodexBlock ? CODEX_METER_BLOCK_SCOPES : [block.blockScope];
|
|
1651
|
+
const nowMs = Date.now();
|
|
1625
1652
|
const upsert = this.#db.transaction(() => {
|
|
1653
|
+
// A mark already holds the write lock: sweep expired rows in the same transaction.
|
|
1654
|
+
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
1626
1655
|
for (const blockScope of blockScopes) {
|
|
1627
1656
|
this.#upsertCredentialBlockStmt.run(
|
|
1628
1657
|
block.credentialId,
|
|
@@ -1633,8 +1662,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1633
1662
|
}
|
|
1634
1663
|
});
|
|
1635
1664
|
upsert.immediate();
|
|
1665
|
+
this.#lastCredentialBlockSweepMs = nowMs;
|
|
1666
|
+
this.#pruneCredentialBlockReconcileAfter(nowMs);
|
|
1636
1667
|
|
|
1637
|
-
const reconcileAfterMs = Math.min(block.blockedUntilMs,
|
|
1668
|
+
const reconcileAfterMs = Math.min(block.blockedUntilMs, nowMs + USAGE_REPORT_TTL_MS);
|
|
1638
1669
|
for (const blockScope of blockScopes) {
|
|
1639
1670
|
this.#credentialBlockReconcileAfter.set(
|
|
1640
1671
|
`${block.credentialId}\0${block.providerKey}\0${blockScope}`,
|
|
@@ -1661,38 +1692,49 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1661
1692
|
}
|
|
1662
1693
|
|
|
1663
1694
|
cleanExpiredCredentialBlocks(nowMs: number): void {
|
|
1695
|
+
// Stamp wall-clock time first: callers may pass a future `nowMs`, and a
|
|
1696
|
+
// failed sweep must not be retried by every following read.
|
|
1697
|
+
this.#lastCredentialBlockSweepMs = Date.now();
|
|
1664
1698
|
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
1699
|
+
this.#pruneCredentialBlockReconcileAfter(nowMs);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
#pruneCredentialBlockReconcileAfter(nowMs: number): void {
|
|
1665
1703
|
for (const [key, reconcileAfterMs] of this.#credentialBlockReconcileAfter) {
|
|
1666
1704
|
if (reconcileAfterMs <= nowMs) this.#credentialBlockReconcileAfter.delete(key);
|
|
1667
1705
|
}
|
|
1668
1706
|
}
|
|
1669
1707
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1708
|
+
/**
|
|
1709
|
+
* Read-path expiry sweep, at most once per {@link CREDENTIAL_BLOCK_SWEEP_INTERVAL_MS}.
|
|
1710
|
+
* A busy write lock only postpones housekeeping; it never fails the read.
|
|
1711
|
+
*/
|
|
1712
|
+
#sweepExpiredCredentialBlocks(): void {
|
|
1672
1713
|
const nowMs = Date.now();
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
if (
|
|
1678
|
-
seenCredentialIds.add(credentialId);
|
|
1679
|
-
const rows = this.#listCredentialBlocksByCredentialStmt.all(
|
|
1680
|
-
credentialId,
|
|
1681
|
-
nowMs,
|
|
1682
|
-
LEGACY_CODEX_BLOCK_PROVIDER_KEY,
|
|
1683
|
-
LEGACY_CODEX_BLOCK_SCOPE,
|
|
1684
|
-
) as CredentialBlockRow[];
|
|
1685
|
-
for (const row of rows) {
|
|
1686
|
-
blocks.push({
|
|
1687
|
-
credentialId: row.credential_id,
|
|
1688
|
-
providerKey: row.provider_key,
|
|
1689
|
-
blockScope: row.block_scope,
|
|
1690
|
-
blockedUntilMs: row.blocked_until_ms,
|
|
1691
|
-
updatedAtMs: row.updated_at * 1000,
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1714
|
+
if (nowMs - this.#lastCredentialBlockSweepMs < CREDENTIAL_BLOCK_SWEEP_INTERVAL_MS) return;
|
|
1715
|
+
try {
|
|
1716
|
+
this.cleanExpiredCredentialBlocks(nowMs);
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
if (!isSqliteBusyError(err)) throw err;
|
|
1694
1719
|
}
|
|
1695
|
-
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
|
|
1723
|
+
if (credentialIds.length === 0) return [];
|
|
1724
|
+
this.#sweepExpiredCredentialBlocks();
|
|
1725
|
+
const rows = this.#listCredentialBlocksStmt.all(
|
|
1726
|
+
JSON.stringify([...new Set(credentialIds)]),
|
|
1727
|
+
Date.now(),
|
|
1728
|
+
LEGACY_CODEX_BLOCK_PROVIDER_KEY,
|
|
1729
|
+
LEGACY_CODEX_BLOCK_SCOPE,
|
|
1730
|
+
) as CredentialBlockRow[];
|
|
1731
|
+
return rows.map(row => ({
|
|
1732
|
+
credentialId: row.credential_id,
|
|
1733
|
+
providerKey: row.provider_key,
|
|
1734
|
+
blockScope: row.block_scope,
|
|
1735
|
+
blockedUntilMs: row.blocked_until_ms,
|
|
1736
|
+
updatedAtMs: row.updated_at * 1000,
|
|
1737
|
+
}));
|
|
1696
1738
|
}
|
|
1697
1739
|
|
|
1698
1740
|
tryAcquireCredentialRefreshLease(credentialId: number, owner: string, expiresAtMs: number): boolean {
|
|
@@ -2050,7 +2092,8 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
2050
2092
|
this.#upsertCacheStmt.finalize();
|
|
2051
2093
|
this.#deleteExpiredCacheStmt.finalize();
|
|
2052
2094
|
this.#getCredentialBlockStmt.finalize();
|
|
2053
|
-
this.#
|
|
2095
|
+
this.#listCredentialBlockScopesStmt.finalize();
|
|
2096
|
+
this.#listCredentialBlocksStmt.finalize();
|
|
2054
2097
|
this.#upsertCredentialBlockStmt.finalize();
|
|
2055
2098
|
this.#deleteCredentialBlocksStmt.finalize();
|
|
2056
2099
|
this.#deleteCredentialBlockStmt.finalize();
|
package/src/auth/store.ts
CHANGED
|
@@ -98,6 +98,12 @@ export interface CredentialCacheStore {
|
|
|
98
98
|
export interface CredentialBlockStore {
|
|
99
99
|
/** Non-expired block for one (credential, providerKey, scope) key, or undefined. */
|
|
100
100
|
getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* Non-expired blocks for one credential and providerKey, keyed by block
|
|
103
|
+
* scope (`""` is unscoped), in one read. Optional: selectors fall back to
|
|
104
|
+
* {@link CredentialBlockStore.getCredentialBlock} per scope when absent.
|
|
105
|
+
*/
|
|
106
|
+
getCredentialBlockScopes?(credentialId: number, providerKey: string): ReadonlyMap<string, number>;
|
|
101
107
|
/** Earliest time a shared-store block should be eligible for live-usage reconciliation. */
|
|
102
108
|
getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined;
|
|
103
109
|
/** Upsert with MAX semantics: keep the later blockedUntilMs on conflict. */
|
package/src/auth/usage-cache.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { UsageCredential, UsageProvider, UsageReport } from "../usage";
|
|
|
6
6
|
|
|
7
7
|
const USAGE_CACHE_PREFIX = "usage_cache:";
|
|
8
8
|
const USAGE_FORCE_REFRESH_CACHE_PREFIX = "force-refresh:";
|
|
9
|
+
const USAGE_REPORT_KEY_PREFIX = "report:";
|
|
9
10
|
/** Minimum interval between non-exhausted header snapshots; used by UsageService. */
|
|
10
11
|
export const USAGE_HEADER_INGEST_INTERVAL_MS = 60_000;
|
|
11
12
|
const USAGE_LAST_GOOD_RETENTION_MS = 24 * 60 * 60_000;
|
|
@@ -45,6 +46,13 @@ function parseUsageCacheEntry<T>(raw: string): UsageCacheEntry<T> | undefined {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/** Cached reports drop the provider's upstream `raw` payload; only a fresh fetch carries it. */
|
|
50
|
+
function withoutRawPayload(value: unknown): unknown {
|
|
51
|
+
if (value === null || typeof value !== "object" || !("raw" in value)) return value;
|
|
52
|
+
const { raw: _raw, ...rest } = value;
|
|
53
|
+
return rest;
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
/** Convert a stored credential for usage providers; used by usage and health services. */
|
|
49
57
|
export function buildUsageCredential(credential: AuthCredential): UsageCredential {
|
|
50
58
|
if (credential.type === "api_key") {
|
|
@@ -196,12 +204,13 @@ export class UsageCache {
|
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
set<T>(key: string, entry: UsageCacheEntry<T>): void {
|
|
207
|
+
const value = withoutRawPayload(entry.value);
|
|
199
208
|
const payload = JSON.stringify({
|
|
200
|
-
value
|
|
209
|
+
value,
|
|
201
210
|
expiresAt: entry.expiresAt,
|
|
202
211
|
});
|
|
203
212
|
const durableExpiresAt =
|
|
204
|
-
|
|
213
|
+
value === null ? entry.expiresAt : Math.max(entry.expiresAt, Date.now() + USAGE_LAST_GOOD_RETENTION_MS);
|
|
205
214
|
this.#store.setCache(`${USAGE_CACHE_PREFIX}${key}`, payload, Math.floor(durableExpiresAt / 1000));
|
|
206
215
|
}
|
|
207
216
|
|
|
@@ -272,7 +281,7 @@ export class UsageCache {
|
|
|
272
281
|
const baseUrl = this.#normalizeUsageBaseUrl(request.baseUrl) || "default";
|
|
273
282
|
const identity = usageCacheIdentity(request.credential);
|
|
274
283
|
const providerKey = this.#usageCacheProviderKey(request.provider);
|
|
275
|
-
const cacheKey =
|
|
284
|
+
const cacheKey = `${USAGE_REPORT_KEY_PREFIX}${providerKey}:${baseUrl}:${identity}`;
|
|
276
285
|
const cacheKeys = this.#usageReportCacheKeysByProvider.get(request.provider) ?? new Set<string>();
|
|
277
286
|
cacheKeys.add(cacheKey);
|
|
278
287
|
this.#usageReportCacheKeysByProvider.set(request.provider, cacheKeys);
|
|
@@ -334,7 +343,7 @@ export class UsageCache {
|
|
|
334
343
|
this.#epoch += 1;
|
|
335
344
|
this.#bumpRefreshEpoch(provider);
|
|
336
345
|
const expired = Date.now() - 1;
|
|
337
|
-
const prefix =
|
|
346
|
+
const prefix = `${USAGE_REPORT_KEY_PREFIX}${this.#usageCacheProviderKey(provider)}:`;
|
|
338
347
|
if (this.deletePrefix(prefix)) return;
|
|
339
348
|
const cacheKeys = new Set(this.#usageReportCacheKeysByProvider.get(provider));
|
|
340
349
|
for (const entry of this.#pool.entries(provider)) {
|
|
@@ -361,7 +370,9 @@ export class UsageCache {
|
|
|
361
370
|
): Promise<void> {
|
|
362
371
|
this.#epoch += 1;
|
|
363
372
|
this.#bumpRefreshEpoch(provider);
|
|
364
|
-
const prefix = provider
|
|
373
|
+
const prefix = provider
|
|
374
|
+
? `${USAGE_REPORT_KEY_PREFIX}${this.#usageCacheProviderKey(provider)}:`
|
|
375
|
+
: USAGE_REPORT_KEY_PREFIX;
|
|
365
376
|
if (!this.deletePrefix(prefix)) {
|
|
366
377
|
// Third-party stores may not support prefix deletion. Clear every active
|
|
367
378
|
// request key instead, including API-key and environment credentials.
|
package/src/auth/usage.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { authPolicyFor } from "@oh-my-pi/pi-catalog/compat/auth";
|
|
2
2
|
import { $pickenv, logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { getEnvApiKey } from "../env-api-key";
|
|
3
4
|
import * as AIError from "../error";
|
|
4
|
-
import { getEnvApiKey } from "../stream";
|
|
5
5
|
import type { OAuthCredentials } from "../registry/oauth/types";
|
|
6
6
|
import type { Provider } from "../types";
|
|
7
7
|
import { resolveUsedFraction } from "../usage";
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import * as AIError from "../error";
|
|
29
29
|
import { AuthBrokerClient, AuthBrokerError } from "./client";
|
|
30
30
|
import { type AuthBrokerAccountPool, RemoteAuthCredentialStore } from "./remote-store";
|
|
31
|
-
import { readAuthBrokerSnapshotCache,
|
|
31
|
+
import { readAuthBrokerSnapshotCache, scheduleAuthBrokerSnapshotCacheWrite } from "./snapshot-cache";
|
|
32
32
|
import { DEFAULT_SNAPSHOT_CACHE_TTL_MS, type SnapshotResponse } from "./types";
|
|
33
33
|
|
|
34
34
|
export interface AuthBrokerClientConfig {
|
|
@@ -396,16 +396,17 @@ export async function openAuthCredentialStore(
|
|
|
396
396
|
const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
|
|
397
397
|
const cachePath = options.cachePath ?? getAuthBrokerSnapshotCachePath();
|
|
398
398
|
const ttlMs = resolveSnapshotTtlMs();
|
|
399
|
+
// Coalesced fire-and-forget: the first snapshot lands immediately (so a
|
|
400
|
+
// fresh boot seeds the cache), bursts collapse into one trailing write,
|
|
401
|
+
// and anything pending is flushed on shutdown.
|
|
399
402
|
const persist =
|
|
400
403
|
ttlMs > 0
|
|
401
404
|
? (snapshot: SnapshotResponse): void => {
|
|
402
|
-
|
|
405
|
+
scheduleAuthBrokerSnapshotCacheWrite({
|
|
403
406
|
path: cachePath,
|
|
404
407
|
token: brokerConfig.token,
|
|
405
408
|
url: brokerConfig.url,
|
|
406
409
|
snapshot,
|
|
407
|
-
}).catch(error => {
|
|
408
|
-
logger.debug("auth-broker snapshot cache write failed", { error: String(error) });
|
|
409
410
|
});
|
|
410
411
|
}
|
|
411
412
|
: undefined;
|