sidekick-shared 0.21.1 → 0.21.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -45
- package/dist/accountManager.js +9 -7
- package/dist/accountRegistry.js +18 -14
- package/dist/accountStatus.js +4 -1
- package/dist/accounts.js +60 -25
- package/dist/aggregation/EventAggregator.d.ts +14 -11
- package/dist/aggregation/EventAggregator.js +68 -31
- package/dist/aggregation/PatternExtractor.js +5 -4
- package/dist/autoSwitch.js +6 -12
- package/dist/claudeProfiles.js +3 -3
- package/dist/codexProfiles.js +45 -22
- package/dist/codexQuota.js +44 -16
- package/dist/codexQuotaWatcher.js +10 -3
- package/dist/context/composer.js +8 -6
- package/dist/context/sessionContext.js +12 -9
- package/dist/credentialIO.js +10 -8
- package/dist/extractors/sessionAssets.js +9 -1
- package/dist/extractors/sources/claudeAssets.js +7 -1
- package/dist/extractors/sources/codexAssets.js +14 -3
- package/dist/extractors/toolCall.js +1 -4
- package/dist/formatters/eventHighlighter.js +148 -28
- package/dist/formatters/noiseClassifier.js +3 -11
- package/dist/formatters/sessionDump.js +22 -13
- package/dist/formatters/toolSummary.js +1 -3
- package/dist/index.d.ts +25 -23
- package/dist/index.js +12 -3
- package/dist/modelContext.js +10 -8
- package/dist/modelInfo.js +11 -6
- package/dist/multiProviderQuotaService.js +10 -7
- package/dist/parsers/codexParser.js +170 -103
- package/dist/parsers/debugLogParser.js +5 -4
- package/dist/parsers/openCodeParser.js +66 -48
- package/dist/parsers/planExtractor.js +3 -3
- package/dist/parsers/sessionActivityDetector.js +10 -6
- package/dist/parsers/sessionPathResolver.js +29 -34
- package/dist/parsers/subagentScanner.js +21 -20
- package/dist/parsers/subagentTraceParser.js +7 -4
- package/dist/phrases.js +11 -11
- package/dist/providerStatus.js +3 -4
- package/dist/providers/claudeCode.js +3 -1
- package/dist/providers/codex.js +35 -14
- package/dist/providers/codexDatabase.js +14 -5
- package/dist/providers/detect.js +10 -6
- package/dist/providers/openCode.d.ts +4 -4
- package/dist/providers/openCode.js +96 -68
- package/dist/providers/openCodeDatabase.js +44 -17
- package/dist/quota.d.ts +19 -0
- package/dist/quota.js +50 -8
- package/dist/quotaHistory.js +10 -3
- package/dist/quotaSnapshots.d.ts +1 -1
- package/dist/quotaSnapshots.js +2 -2
- package/dist/readers/decisions.js +1 -1
- package/dist/readers/notes.js +5 -3
- package/dist/readers/plans.js +43 -23
- package/dist/readers/tasks.js +1 -1
- package/dist/report/htmlHelpers.js +135 -16
- package/dist/report/htmlReportGenerator.js +38 -12
- package/dist/report/transcriptParser.js +8 -2
- package/dist/schemas/index.d.ts +4 -4
- package/dist/schemas/quota.js +2 -10
- package/dist/schemas/sessionEvent.js +20 -10
- package/dist/search/advancedFilter.js +3 -3
- package/dist/search/sessionSearch.js +10 -4
- package/dist/terminalSync.js +4 -2
- package/dist/turns/assistantTurn.js +5 -1
- package/dist/watchers/eventBridge.js +56 -26
- package/dist/watchers/factory.js +5 -2
- package/dist/watchers/jsonlTail.js +3 -1
- package/dist/watchers/jsonlWatcher.js +128 -52
- package/dist/watchers/providerReaderWatcher.js +3 -1
- package/dist/watchers/sqliteWatcher.js +52 -22
- package/dist/zaiQuota.d.ts +6 -6
- package/dist/zaiQuota.js +6 -10
- package/dist/zaiQuotaApi.d.ts +33 -0
- package/dist/zaiQuotaApi.js +279 -0
- package/dist/zaiQuotaWatcher.js +10 -9
- package/package.json +5 -2
|
@@ -63,18 +63,30 @@ function normalizeMessage(msg) {
|
|
|
63
63
|
const model = data.modelID || undefined;
|
|
64
64
|
if (role === 'user') {
|
|
65
65
|
events.push({
|
|
66
|
-
providerId: 'opencode',
|
|
67
|
-
|
|
66
|
+
providerId: 'opencode',
|
|
67
|
+
type: 'user',
|
|
68
|
+
timestamp: ts,
|
|
69
|
+
summary: content || '(user message)',
|
|
70
|
+
model,
|
|
71
|
+
raw: data,
|
|
68
72
|
});
|
|
69
73
|
}
|
|
70
74
|
else if (role === 'assistant') {
|
|
71
75
|
events.push({
|
|
72
|
-
providerId: 'opencode',
|
|
73
|
-
|
|
76
|
+
providerId: 'opencode',
|
|
77
|
+
type: 'assistant',
|
|
78
|
+
timestamp: ts,
|
|
79
|
+
summary: content || '(thinking...)',
|
|
80
|
+
tokens,
|
|
81
|
+
cost,
|
|
82
|
+
model,
|
|
83
|
+
raw: data,
|
|
74
84
|
});
|
|
75
85
|
}
|
|
76
86
|
}
|
|
77
|
-
catch {
|
|
87
|
+
catch {
|
|
88
|
+
/* skip malformed */
|
|
89
|
+
}
|
|
78
90
|
return events;
|
|
79
91
|
}
|
|
80
92
|
function normalizePart(part) {
|
|
@@ -93,13 +105,15 @@ function normalizePart(part) {
|
|
|
93
105
|
const toolInput = state?.input;
|
|
94
106
|
const input = data.args ? truncate(JSON.stringify(data.args), 80) : '';
|
|
95
107
|
// Include input in raw so PlanExtractor can access raw.input
|
|
96
|
-
const toolRaw = eventType === 'tool_use'
|
|
97
|
-
? { ...data, input: toolInput || {} }
|
|
98
|
-
: data;
|
|
108
|
+
const toolRaw = eventType === 'tool_use' ? { ...data, input: toolInput || {} } : data;
|
|
99
109
|
events.push({
|
|
100
|
-
providerId: 'opencode',
|
|
110
|
+
providerId: 'opencode',
|
|
111
|
+
type: eventType,
|
|
112
|
+
timestamp: ts,
|
|
101
113
|
summary: input ? `${toolName} ${input}` : toolName,
|
|
102
|
-
toolName,
|
|
114
|
+
toolName,
|
|
115
|
+
toolInput: input || undefined,
|
|
116
|
+
raw: toolRaw,
|
|
103
117
|
});
|
|
104
118
|
}
|
|
105
119
|
else if (type === 'text') {
|
|
@@ -107,7 +121,9 @@ function normalizePart(part) {
|
|
|
107
121
|
if (text) {
|
|
108
122
|
// Wrap raw to include message.content for <proposed_plan> extraction
|
|
109
123
|
events.push({
|
|
110
|
-
providerId: 'opencode',
|
|
124
|
+
providerId: 'opencode',
|
|
125
|
+
type: 'assistant',
|
|
126
|
+
timestamp: ts,
|
|
111
127
|
summary: truncate(text, 200),
|
|
112
128
|
raw: { ...data, message: { content: [{ type: 'text', text }] } },
|
|
113
129
|
});
|
|
@@ -116,7 +132,9 @@ function normalizePart(part) {
|
|
|
116
132
|
if (direction) {
|
|
117
133
|
const syntheticToolName = direction === 'enter' ? 'EnterPlanMode' : 'ExitPlanMode';
|
|
118
134
|
events.push({
|
|
119
|
-
providerId: 'opencode',
|
|
135
|
+
providerId: 'opencode',
|
|
136
|
+
type: 'tool_use',
|
|
137
|
+
timestamp: ts,
|
|
120
138
|
summary: syntheticToolName,
|
|
121
139
|
toolName: syntheticToolName,
|
|
122
140
|
raw: { input: { source: 'opencode_text_heuristic' } },
|
|
@@ -125,7 +143,9 @@ function normalizePart(part) {
|
|
|
125
143
|
}
|
|
126
144
|
}
|
|
127
145
|
}
|
|
128
|
-
catch {
|
|
146
|
+
catch {
|
|
147
|
+
/* skip malformed */
|
|
148
|
+
}
|
|
129
149
|
return events;
|
|
130
150
|
}
|
|
131
151
|
function extractContent(data) {
|
|
@@ -158,7 +178,9 @@ class SqliteSessionWatcher {
|
|
|
158
178
|
this.callbacks = callbacks;
|
|
159
179
|
this.db = new openCodeDatabase_1.OpenCodeDatabase(path.dirname(dbPath));
|
|
160
180
|
}
|
|
161
|
-
get isActive() {
|
|
181
|
+
get isActive() {
|
|
182
|
+
return this._isActive;
|
|
183
|
+
}
|
|
162
184
|
/** Seek to a timestamp position. Must be called before start(). */
|
|
163
185
|
seekTo(position) {
|
|
164
186
|
this.lastMessageTime = position;
|
|
@@ -186,8 +208,12 @@ class SqliteSessionWatcher {
|
|
|
186
208
|
this.skipToEnd();
|
|
187
209
|
}
|
|
188
210
|
// Watch db and wal files for changes
|
|
189
|
-
this.watchFile(this.dbPath, (w) => {
|
|
190
|
-
|
|
211
|
+
this.watchFile(this.dbPath, (w) => {
|
|
212
|
+
this.fsWatcher = w;
|
|
213
|
+
});
|
|
214
|
+
this.watchFile(this.dbPath + '-wal', (w) => {
|
|
215
|
+
this.walWatcher = w;
|
|
216
|
+
});
|
|
191
217
|
// Fallback polling
|
|
192
218
|
this.pollTimer = setInterval(() => {
|
|
193
219
|
if (this._isActive)
|
|
@@ -223,10 +249,14 @@ class SqliteSessionWatcher {
|
|
|
223
249
|
const watcher = fs.watch(filePath, { persistent: false }, () => {
|
|
224
250
|
this.debouncedPoll();
|
|
225
251
|
});
|
|
226
|
-
watcher.on('error', () => {
|
|
252
|
+
watcher.on('error', () => {
|
|
253
|
+
/* ignore watch errors */
|
|
254
|
+
});
|
|
227
255
|
setter(watcher);
|
|
228
256
|
}
|
|
229
|
-
catch {
|
|
257
|
+
catch {
|
|
258
|
+
/* fs.watch not available */
|
|
259
|
+
}
|
|
230
260
|
}
|
|
231
261
|
debouncedPoll() {
|
|
232
262
|
if (this.debounceTimer)
|
|
@@ -239,10 +269,10 @@ class SqliteSessionWatcher {
|
|
|
239
269
|
const messages = this.db.getMessagesForSession(this.sessionId);
|
|
240
270
|
const parts = this.db.getPartsForSession(this.sessionId);
|
|
241
271
|
if (messages.length > 0) {
|
|
242
|
-
this.lastMessageTime = Math.max(...messages.map(m => m.time_created));
|
|
272
|
+
this.lastMessageTime = Math.max(...messages.map((m) => m.time_created));
|
|
243
273
|
}
|
|
244
274
|
if (parts.length > 0) {
|
|
245
|
-
this.lastPartTime = Math.max(...parts.map(p => p.time_created));
|
|
275
|
+
this.lastPartTime = Math.max(...parts.map((p) => p.time_created));
|
|
246
276
|
}
|
|
247
277
|
}
|
|
248
278
|
pollNewData() {
|
|
@@ -252,7 +282,7 @@ class SqliteSessionWatcher {
|
|
|
252
282
|
const messages = this.db.getMessagesForSession(this.sessionId);
|
|
253
283
|
const parts = this.db.getPartsForSession(this.sessionId);
|
|
254
284
|
// Emit new messages
|
|
255
|
-
const newMessages = messages.filter(m => m.time_created > this.lastMessageTime);
|
|
285
|
+
const newMessages = messages.filter((m) => m.time_created > this.lastMessageTime);
|
|
256
286
|
for (const msg of newMessages) {
|
|
257
287
|
const events = normalizeMessage(msg);
|
|
258
288
|
for (const e of events)
|
|
@@ -261,7 +291,7 @@ class SqliteSessionWatcher {
|
|
|
261
291
|
this.lastMessageTime = msg.time_created;
|
|
262
292
|
}
|
|
263
293
|
// Emit new parts
|
|
264
|
-
const newParts = parts.filter(p => p.time_created > this.lastPartTime);
|
|
294
|
+
const newParts = parts.filter((p) => p.time_created > this.lastPartTime);
|
|
265
295
|
for (const part of newParts) {
|
|
266
296
|
const events = normalizePart(part);
|
|
267
297
|
for (const e of events)
|
package/dist/zaiQuota.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Deprecated stateless quota accumulator for z.ai coding plans.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* The
|
|
4
|
+
* This estimator is kept for compatibility with consumers that imported the
|
|
5
|
+
* old helpers directly. Product code should use `zaiQuotaApi.ts`, which calls
|
|
6
|
+
* z.ai's authoritative quota endpoint. The legacy estimator only uses:
|
|
7
7
|
*
|
|
8
8
|
* 1. Per-call token `usage` returned inline on each chat completion —
|
|
9
9
|
* which OpenCode persists into `opencode.db` message rows tagged with
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* in the human-readable message text.
|
|
14
14
|
*
|
|
15
15
|
* This module is therefore a **derivation** rather than a fetcher: callers
|
|
16
|
-
* feed it observed assistant turns (and any error events), and it computes
|
|
17
|
-
*
|
|
16
|
+
* feed it observed assistant turns (and any error events), and it computes a
|
|
17
|
+
* compatibility `QuotaState` keyed against the published per-tier budgets.
|
|
18
18
|
*
|
|
19
19
|
* Polling / eventing / persistence are the caller's responsibility —
|
|
20
20
|
* see `ZaiQuotaWatcher` for the bundled watcher.
|
package/dist/zaiQuota.js
CHANGED
|
@@ -132,18 +132,14 @@ function resolveZaiTier(configured, accumulated) {
|
|
|
132
132
|
function inferZaiQuotaState(accumulated, tier, options = {}) {
|
|
133
133
|
const budget = exports.ZAI_TIER_BUDGETS[tier];
|
|
134
134
|
const capturedAt = options.capturedAt ?? new Date().toISOString();
|
|
135
|
-
const fiveHourUtil = budget.fiveHour > 0
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
? Math.min((accumulated.weeklyPrompts / budget.weekly) * 100, 200)
|
|
140
|
-
: 0;
|
|
141
|
-
const fiveHourResetsAt = options.authoritativeFiveHourResetAt
|
|
142
|
-
?? (accumulated.fiveHourStartedAtMs !== null
|
|
135
|
+
const fiveHourUtil = budget.fiveHour > 0 ? Math.min((accumulated.fiveHourPrompts / budget.fiveHour) * 100, 200) : 0;
|
|
136
|
+
const weeklyUtil = budget.weekly > 0 ? Math.min((accumulated.weeklyPrompts / budget.weekly) * 100, 200) : 0;
|
|
137
|
+
const fiveHourResetsAt = options.authoritativeFiveHourResetAt ??
|
|
138
|
+
(accumulated.fiveHourStartedAtMs !== null
|
|
143
139
|
? new Date(accumulated.fiveHourStartedAtMs + FIVE_HOUR_MS).toISOString()
|
|
144
140
|
: '');
|
|
145
|
-
const sevenDayResetsAt = options.authoritativeWeeklyResetAt
|
|
146
|
-
|
|
141
|
+
const sevenDayResetsAt = options.authoritativeWeeklyResetAt ??
|
|
142
|
+
(accumulated.weeklyStartedAtMs !== null
|
|
147
143
|
? new Date(accumulated.weeklyStartedAtMs + SEVEN_DAY_MS).toISOString()
|
|
148
144
|
: '');
|
|
149
145
|
const fiveHour = {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { QuotaState } from './quota';
|
|
2
|
+
import type { ProviderQuotaState } from './providerQuota';
|
|
3
|
+
type ZaiSnapshotProvider = 'zai';
|
|
4
|
+
type SnapshotReader = (providerId: ZaiSnapshotProvider, accountId: string) => QuotaState | null;
|
|
5
|
+
type SnapshotWriter = (providerId: ZaiSnapshotProvider, accountId: string, quota: QuotaState) => void;
|
|
6
|
+
export type ZaiPlatform = 'ZAI' | 'ZHIPU';
|
|
7
|
+
export type ZaiCredentialSource = 'opencode' | 'env';
|
|
8
|
+
export interface ZaiCredentials {
|
|
9
|
+
authToken: string;
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
platform: ZaiPlatform;
|
|
12
|
+
source: ZaiCredentialSource;
|
|
13
|
+
}
|
|
14
|
+
export interface ReadZaiCredentialsOptions {
|
|
15
|
+
openCodeDataDir?: string;
|
|
16
|
+
env?: Record<string, string | undefined>;
|
|
17
|
+
}
|
|
18
|
+
export interface ZaiQuotaApiOptions extends ReadZaiCredentialsOptions {
|
|
19
|
+
credentials?: ZaiCredentials | null;
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
capturedAt?: string;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface ZaiQuotaResolveOptions extends ZaiQuotaApiOptions {
|
|
25
|
+
accountId?: string;
|
|
26
|
+
readSnapshot?: SnapshotReader;
|
|
27
|
+
writeSnapshot?: SnapshotWriter;
|
|
28
|
+
}
|
|
29
|
+
export declare function readZaiCredentials(options?: ReadZaiCredentialsOptions): ZaiCredentials | null;
|
|
30
|
+
export declare function quotaStateFromZaiQuotaLimitPayload(payload: unknown, capturedAt?: string): QuotaState;
|
|
31
|
+
export declare function fetchZaiQuotaFromApi(options?: ZaiQuotaApiOptions): Promise<QuotaState>;
|
|
32
|
+
export declare function resolveZaiQuota(options?: ZaiQuotaResolveOptions): Promise<ProviderQuotaState<'zai'>>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readZaiCredentials = readZaiCredentials;
|
|
7
|
+
exports.quotaStateFromZaiQuotaLimitPayload = quotaStateFromZaiQuotaLimitPayload;
|
|
8
|
+
exports.fetchZaiQuotaFromApi = fetchZaiQuotaFromApi;
|
|
9
|
+
exports.resolveZaiQuota = resolveZaiQuota;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const quotaSnapshots_1 = require("./quotaSnapshots");
|
|
14
|
+
const quota_1 = require("./quota");
|
|
15
|
+
const DEFAULT_ZAI_BASE_URL = 'https://api.z.ai/api/anthropic';
|
|
16
|
+
const QUOTA_PATH = '/api/monitor/usage/quota/limit';
|
|
17
|
+
const DEFAULT_ACCOUNT_ID = 'default';
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
19
|
+
function unavailableZaiQuotaState(error, meta = {}, capturedAt = new Date().toISOString()) {
|
|
20
|
+
return {
|
|
21
|
+
fiveHour: { utilization: 0, resetsAt: '' },
|
|
22
|
+
sevenDay: { utilization: 0, resetsAt: '' },
|
|
23
|
+
available: false,
|
|
24
|
+
error,
|
|
25
|
+
providerId: 'zai',
|
|
26
|
+
source: 'api',
|
|
27
|
+
capturedAt,
|
|
28
|
+
fiveHourLabel: '5-Hour',
|
|
29
|
+
sevenDayLabel: 'Weekly',
|
|
30
|
+
...meta,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function openCodeDataDirCandidates() {
|
|
34
|
+
const candidates = [];
|
|
35
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
36
|
+
if (xdg)
|
|
37
|
+
candidates.push(path_1.default.join(xdg, 'opencode'));
|
|
38
|
+
candidates.push(path_1.default.join(os_1.default.homedir(), '.local', 'share', 'opencode'));
|
|
39
|
+
if (process.platform === 'darwin') {
|
|
40
|
+
candidates.push(path_1.default.join(os_1.default.homedir(), 'Library', 'Application Support', 'opencode'));
|
|
41
|
+
}
|
|
42
|
+
else if (process.platform === 'win32') {
|
|
43
|
+
candidates.push(path_1.default.join(process.env.LOCALAPPDATA ||
|
|
44
|
+
process.env.APPDATA ||
|
|
45
|
+
path_1.default.join(os_1.default.homedir(), 'AppData', 'Local'), 'opencode'));
|
|
46
|
+
}
|
|
47
|
+
return Array.from(new Set(candidates));
|
|
48
|
+
}
|
|
49
|
+
function readOpenCodeAuthToken(openCodeDataDir) {
|
|
50
|
+
const candidates = openCodeDataDir ? [openCodeDataDir] : openCodeDataDirCandidates();
|
|
51
|
+
for (const dataDir of candidates) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dataDir, 'auth.json'), 'utf8'));
|
|
54
|
+
const codingPlan = parsed['zai-coding-plan'];
|
|
55
|
+
const zai = parsed.zai;
|
|
56
|
+
if (typeof codingPlan?.key === 'string' && codingPlan.key.trim())
|
|
57
|
+
return codingPlan.key.trim();
|
|
58
|
+
if (typeof zai?.key === 'string' && zai.key.trim())
|
|
59
|
+
return zai.key.trim();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Try the next candidate.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function platformFromBaseUrl(baseUrl) {
|
|
68
|
+
if (baseUrl.includes('api.z.ai'))
|
|
69
|
+
return 'ZAI';
|
|
70
|
+
if (baseUrl.includes('open.bigmodel.cn') || baseUrl.includes('dev.bigmodel.cn'))
|
|
71
|
+
return 'ZHIPU';
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function readZaiCredentials(options = {}) {
|
|
75
|
+
const openCodeToken = readOpenCodeAuthToken(options.openCodeDataDir);
|
|
76
|
+
if (openCodeToken) {
|
|
77
|
+
return {
|
|
78
|
+
authToken: openCodeToken,
|
|
79
|
+
baseUrl: DEFAULT_ZAI_BASE_URL,
|
|
80
|
+
platform: 'ZAI',
|
|
81
|
+
source: 'opencode',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const env = options.env ?? process.env;
|
|
85
|
+
const authToken = env.ANTHROPIC_AUTH_TOKEN?.trim();
|
|
86
|
+
const baseUrl = env.ANTHROPIC_BASE_URL?.trim();
|
|
87
|
+
if (!authToken || !baseUrl)
|
|
88
|
+
return null;
|
|
89
|
+
const platform = platformFromBaseUrl(baseUrl);
|
|
90
|
+
if (!platform)
|
|
91
|
+
return null;
|
|
92
|
+
return {
|
|
93
|
+
authToken,
|
|
94
|
+
baseUrl,
|
|
95
|
+
platform,
|
|
96
|
+
source: 'env',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function quotaLimitUrl(baseUrl) {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = new URL(baseUrl);
|
|
102
|
+
return `${parsed.protocol}//${parsed.host}${QUOTA_PATH}`;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function tokenLimitEntries(payload) {
|
|
109
|
+
const limits = payload.data?.limits ?? payload.limits ?? [];
|
|
110
|
+
return limits.filter((item) => item?.type === 'TOKENS_LIMIT' && typeof item.percentage === 'number');
|
|
111
|
+
}
|
|
112
|
+
function isFiveHourLimit(item) {
|
|
113
|
+
return item.type === 'TOKENS_LIMIT' && item.unit === 3 && item.number === 5;
|
|
114
|
+
}
|
|
115
|
+
function isWeeklyLimit(item) {
|
|
116
|
+
return item.type === 'TOKENS_LIMIT' && item.unit === 6 && item.number === 1;
|
|
117
|
+
}
|
|
118
|
+
function isoFromEpochMs(value) {
|
|
119
|
+
if (value == null)
|
|
120
|
+
return '';
|
|
121
|
+
const ms = typeof value === 'number' ? value : Number(value);
|
|
122
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
123
|
+
return '';
|
|
124
|
+
return new Date(ms).toISOString();
|
|
125
|
+
}
|
|
126
|
+
function displayPlanName(level) {
|
|
127
|
+
if (!level)
|
|
128
|
+
return 'z.ai Coding Plan';
|
|
129
|
+
return `z.ai ${level.charAt(0).toUpperCase()}${level.slice(1)}`;
|
|
130
|
+
}
|
|
131
|
+
function quotaStateFromZaiQuotaLimitPayload(payload, capturedAt = new Date().toISOString()) {
|
|
132
|
+
const data = payload;
|
|
133
|
+
const tokenLimits = tokenLimitEntries(data);
|
|
134
|
+
const fiveHourLimit = tokenLimits.find(isFiveHourLimit) ?? tokenLimits[0];
|
|
135
|
+
const weeklyLimit = tokenLimits.find(isWeeklyLimit) ?? tokenLimits.find((item) => item !== fiveHourLimit);
|
|
136
|
+
const level = data.data?.level;
|
|
137
|
+
if (!fiveHourLimit || !weeklyLimit) {
|
|
138
|
+
return unavailableZaiQuotaState('z.ai quota API returned no token quota windows.', {
|
|
139
|
+
failureKind: 'unknown',
|
|
140
|
+
}, capturedAt);
|
|
141
|
+
}
|
|
142
|
+
return (0, quota_1.withQuotaProjections)({
|
|
143
|
+
fiveHour: {
|
|
144
|
+
utilization: fiveHourLimit.percentage ?? 0,
|
|
145
|
+
resetsAt: isoFromEpochMs(fiveHourLimit.nextResetTime),
|
|
146
|
+
},
|
|
147
|
+
sevenDay: {
|
|
148
|
+
utilization: weeklyLimit.percentage ?? 0,
|
|
149
|
+
resetsAt: isoFromEpochMs(weeklyLimit.nextResetTime),
|
|
150
|
+
},
|
|
151
|
+
available: true,
|
|
152
|
+
providerId: 'zai',
|
|
153
|
+
source: 'api',
|
|
154
|
+
capturedAt,
|
|
155
|
+
fiveHourLabel: '5-Hour',
|
|
156
|
+
sevenDayLabel: 'Weekly',
|
|
157
|
+
planType: level,
|
|
158
|
+
limitId: level ? `zai-${level}` : 'zai-coding-plan',
|
|
159
|
+
limitName: displayPlanName(level),
|
|
160
|
+
}, {
|
|
161
|
+
fiveHourWindowMs: quota_1.FIVE_HOUR_WINDOW_MS,
|
|
162
|
+
sevenDayWindowMs: quota_1.SEVEN_DAY_WINDOW_MS,
|
|
163
|
+
capturedAt,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function parseRetryAfterMs(retryAfter) {
|
|
167
|
+
if (!retryAfter)
|
|
168
|
+
return undefined;
|
|
169
|
+
const seconds = Number(retryAfter);
|
|
170
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
171
|
+
return seconds * 1000;
|
|
172
|
+
const dateMs = Date.parse(retryAfter);
|
|
173
|
+
if (Number.isFinite(dateMs))
|
|
174
|
+
return Math.max(0, dateMs - Date.now());
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
function failureKindForStatus(status) {
|
|
178
|
+
if (status === 401 || status === 403)
|
|
179
|
+
return 'auth';
|
|
180
|
+
if (status === 429)
|
|
181
|
+
return 'rate_limit';
|
|
182
|
+
if (status >= 500)
|
|
183
|
+
return 'server';
|
|
184
|
+
return 'unknown';
|
|
185
|
+
}
|
|
186
|
+
function sanitizeErrorMessage(message, authToken) {
|
|
187
|
+
let result = message;
|
|
188
|
+
if (authToken) {
|
|
189
|
+
result = result.split(authToken).join('<redacted>');
|
|
190
|
+
}
|
|
191
|
+
return result.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/g, 'Bearer <redacted>');
|
|
192
|
+
}
|
|
193
|
+
async function fetchZaiQuotaFromApi(options = {}) {
|
|
194
|
+
const capturedAt = options.capturedAt ?? new Date().toISOString();
|
|
195
|
+
const credentials = options.credentials ?? readZaiCredentials(options);
|
|
196
|
+
if (!credentials) {
|
|
197
|
+
return unavailableZaiQuotaState('No z.ai credentials found. Sign in to OpenCode with z.ai or set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN.', { failureKind: 'auth' }, capturedAt);
|
|
198
|
+
}
|
|
199
|
+
const url = quotaLimitUrl(credentials.baseUrl);
|
|
200
|
+
if (!url) {
|
|
201
|
+
return unavailableZaiQuotaState('Invalid z.ai base URL.', { failureKind: 'unknown' }, capturedAt);
|
|
202
|
+
}
|
|
203
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
204
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
205
|
+
const controller = new AbortController();
|
|
206
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
207
|
+
timeout.unref?.();
|
|
208
|
+
try {
|
|
209
|
+
const response = await fetchImpl(url, {
|
|
210
|
+
method: 'GET',
|
|
211
|
+
headers: {
|
|
212
|
+
Authorization: credentials.authToken,
|
|
213
|
+
'Accept-Language': 'en-US,en',
|
|
214
|
+
Accept: 'application/json',
|
|
215
|
+
'Content-Type': 'application/json',
|
|
216
|
+
},
|
|
217
|
+
signal: controller.signal,
|
|
218
|
+
});
|
|
219
|
+
const body = await response.text();
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = JSON.parse(body);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
parsed = null;
|
|
226
|
+
}
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
const failureKind = failureKindForStatus(response.status);
|
|
229
|
+
const retryAfterMs = parseRetryAfterMs(response.headers.get('retry-after'));
|
|
230
|
+
const detail = parsed && typeof parsed === 'object' && 'msg' in parsed
|
|
231
|
+
? String(parsed.msg ?? '')
|
|
232
|
+
: '';
|
|
233
|
+
const baseMessage = failureKind === 'auth'
|
|
234
|
+
? `z.ai quota API rejected credentials (HTTP ${response.status}).`
|
|
235
|
+
: `z.ai quota API error (HTTP ${response.status}).`;
|
|
236
|
+
return unavailableZaiQuotaState(sanitizeErrorMessage([baseMessage, detail].filter(Boolean).join(' '), credentials.authToken), { failureKind, httpStatus: response.status, retryAfterMs }, capturedAt);
|
|
237
|
+
}
|
|
238
|
+
return quotaStateFromZaiQuotaLimitPayload(parsed, capturedAt);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
const isAbort = error instanceof Error && error.name === 'AbortError';
|
|
242
|
+
return unavailableZaiQuotaState(isAbort ? 'z.ai quota API timed out.' : 'z.ai quota API network error.', { failureKind: 'network' }, capturedAt);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
clearTimeout(timeout);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function enrichZaiQuota(state) {
|
|
249
|
+
const withProjections = (0, quota_1.withQuotaProjections)(state);
|
|
250
|
+
return {
|
|
251
|
+
...withProjections,
|
|
252
|
+
runtimeProvider: 'zai',
|
|
253
|
+
providerId: 'zai',
|
|
254
|
+
fiveHourLabel: state.fiveHourLabel ?? '5-Hour',
|
|
255
|
+
sevenDayLabel: state.sevenDayLabel ?? 'Weekly',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function resolveZaiQuota(options = {}) {
|
|
259
|
+
const accountId = options.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
260
|
+
const readSnapshot = options.readSnapshot ?? quotaSnapshots_1.readQuotaSnapshot;
|
|
261
|
+
const writeSnapshot = options.writeSnapshot ?? quotaSnapshots_1.writeQuotaSnapshot;
|
|
262
|
+
const apiQuota = await fetchZaiQuotaFromApi(options);
|
|
263
|
+
if (apiQuota.available) {
|
|
264
|
+
writeSnapshot('zai', accountId, apiQuota);
|
|
265
|
+
return enrichZaiQuota(apiQuota);
|
|
266
|
+
}
|
|
267
|
+
const cached = readSnapshot('zai', accountId);
|
|
268
|
+
if (cached) {
|
|
269
|
+
return enrichZaiQuota({
|
|
270
|
+
...cached,
|
|
271
|
+
providerId: 'zai',
|
|
272
|
+
source: 'cache',
|
|
273
|
+
stale: true,
|
|
274
|
+
fiveHourLabel: cached.fiveHourLabel ?? '5-Hour',
|
|
275
|
+
sevenDayLabel: cached.sevenDayLabel ?? 'Weekly',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return enrichZaiQuota(apiQuota);
|
|
279
|
+
}
|
package/dist/zaiQuotaWatcher.js
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ZaiQuotaWatcher = void 0;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* observed OpenCode assistant turns.
|
|
5
|
+
* Deprecated event-driven watcher that derives a z.ai coding-plan
|
|
6
|
+
* `QuotaState` from observed OpenCode assistant turns.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* This watcher is retained for compatibility. Product code now uses
|
|
9
|
+
* `zaiQuotaApi.ts` to call z.ai's authoritative quota endpoint. The legacy
|
|
10
|
+
* watcher maintains rolling 5-hour / 7-day windows in memory and recomputes a
|
|
11
|
+
* derived `QuotaState` whenever a new turn arrives. The consumer is
|
|
12
|
+
* responsible for feeding it z.ai-routed assistant turns (and any trapped
|
|
13
|
+
* error events) — typically by subscribing to `SessionMonitor.onSessionEvent`
|
|
14
|
+
* and filtering on `providerID ∈ {zai, zai-coding-plan}`.
|
|
14
15
|
*
|
|
15
16
|
* Optionally writes snapshots + history samples for cross-session cache
|
|
16
17
|
* and trend visualisation, mirroring `CodexQuotaWatcher`.
|
|
@@ -250,7 +251,7 @@ class ZaiQuotaWatcher {
|
|
|
250
251
|
const cutoff = this.now() - SEVEN_DAY_MS;
|
|
251
252
|
if (this.turns.length === 0)
|
|
252
253
|
return;
|
|
253
|
-
const next = this.turns.filter(turn => turn.timestampMs >= cutoff);
|
|
254
|
+
const next = this.turns.filter((turn) => turn.timestampMs >= cutoff);
|
|
254
255
|
if (next.length !== this.turns.length) {
|
|
255
256
|
this.turns = next;
|
|
256
257
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sidekick-shared",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.3",
|
|
4
4
|
"description": "Shared data access layer for Sidekick — readers, types, providers, credentials, quota",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -55,6 +55,8 @@
|
|
|
55
55
|
"clean": "rm -rf dist",
|
|
56
56
|
"lint": "eslint src/",
|
|
57
57
|
"lint:fix": "eslint src/ --fix",
|
|
58
|
+
"format": "prettier --config ../prettier.config.cjs --ignore-path ../.prettierignore --write \"src/**/*.{ts,tsx,js,jsx,json,md,css,html,yml,yaml}\" \"*.{js,cjs,mjs,json,md,yml,yaml}\"",
|
|
59
|
+
"format:check": "prettier --config ../prettier.config.cjs --ignore-path ../.prettierignore --check \"src/**/*.{ts,tsx,js,jsx,json,md,css,html,yml,yaml}\" \"*.{js,cjs,mjs,json,md,yml,yaml}\"",
|
|
58
60
|
"pretest": "npm run build",
|
|
59
61
|
"test": "vitest run",
|
|
60
62
|
"test:watch": "vitest",
|
|
@@ -64,9 +66,10 @@
|
|
|
64
66
|
"@eslint/js": "^9.0.0",
|
|
65
67
|
"@types/node": "^22.0.0",
|
|
66
68
|
"eslint": "^9.0.0",
|
|
69
|
+
"prettier": "3.8.4",
|
|
67
70
|
"typescript": "^5.7.0",
|
|
68
71
|
"typescript-eslint": "^8.0.0",
|
|
69
|
-
"vitest": "^
|
|
72
|
+
"vitest": "^4.1.9"
|
|
70
73
|
},
|
|
71
74
|
"license": "MIT",
|
|
72
75
|
"dependencies": {
|