sidekick-shared 0.27.6 → 0.28.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 +28 -0
- package/README.md +58 -18
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -2
- package/dist/claudePaths.d.ts +11 -0
- package/dist/claudePaths.js +61 -0
- package/dist/claudeProfiles.d.ts +2 -9
- package/dist/claudeProfiles.js +10 -25
- package/dist/codexPaths.d.ts +22 -0
- package/dist/codexPaths.js +67 -0
- package/dist/codexProfiles.d.ts +1 -4
- package/dist/codexProfiles.js +5 -43
- package/dist/doctor.js +2 -2
- package/dist/esm/accountChangeSignal.js +15 -0
- package/dist/esm/accountChanges.js +152 -0
- package/dist/esm/accountHealth.js +288 -0
- package/dist/esm/accountLaunch.js +145 -0
- package/dist/esm/accountLease.js +60 -0
- package/dist/esm/accountManager.js +606 -0
- package/dist/esm/accountRegistry.js +214 -0
- package/dist/esm/accountStatus.js +51 -0
- package/dist/esm/accountSwitch.js +74 -0
- package/dist/esm/accountSync.js +156 -0
- package/dist/esm/accountSyncTypes.js +4 -0
- package/dist/esm/accounts.js +1291 -0
- package/dist/esm/aggregation/EventAggregator.js +2034 -0
- package/dist/esm/aggregation/FrequencyTracker.js +69 -0
- package/dist/esm/aggregation/HeatmapTracker.js +89 -0
- package/dist/esm/aggregation/PatternExtractor.js +168 -0
- package/dist/esm/aggregation/costProvenance.js +31 -0
- package/dist/esm/aggregation/snapshot.js +161 -0
- package/dist/esm/aggregation/types.js +5 -0
- package/dist/esm/analytics/codeImpact.js +18 -0
- package/dist/esm/analytics/compactionLedger.js +37 -0
- package/dist/esm/analytics/qualityScore.js +112 -0
- package/dist/esm/autoSwitch.js +185 -0
- package/dist/esm/browser.js +34 -0
- package/dist/esm/claudeCredentialDoctor.js +239 -0
- package/dist/esm/claudeCredentialLedger.js +91 -0
- package/dist/esm/claudeCredentials.js +111 -0
- package/dist/esm/claudeLiveGuard.js +43 -0
- package/dist/esm/claudePaths.js +22 -0
- package/dist/esm/claudeProfiles.js +142 -0
- package/dist/esm/claudeRefreshLock.js +106 -0
- package/dist/esm/codexAuth.js +101 -0
- package/dist/esm/codexCredentialDoctor.js +265 -0
- package/dist/esm/codexCredentialStore.js +164 -0
- package/dist/esm/codexKeepAlive.js +112 -0
- package/dist/esm/codexPaths.js +172 -0
- package/dist/esm/codexProfiles.js +1251 -0
- package/dist/esm/codexQuota.js +564 -0
- package/dist/esm/codexQuotaWatcher.js +363 -0
- package/dist/esm/context/composer.js +71 -0
- package/dist/esm/context/sessionContext.js +546 -0
- package/dist/esm/credentialIO.js +283 -0
- package/dist/esm/credentials.js +49 -0
- package/dist/esm/doctor.js +305 -0
- package/dist/esm/ensureDefaultAccounts.js +62 -0
- package/dist/esm/errorHistory.js +56 -0
- package/dist/esm/extractors/errorTaxonomy.js +43 -0
- package/dist/esm/extractors/gatherAssets.js +32 -0
- package/dist/esm/extractors/sessionAssets.js +176 -0
- package/dist/esm/extractors/sources/claudeAssets.js +158 -0
- package/dist/esm/extractors/sources/codexAssets.js +234 -0
- package/dist/esm/extractors/tokenUsage.js +41 -0
- package/dist/esm/extractors/toolCall.js +142 -0
- package/dist/esm/failingTools.js +65 -0
- package/dist/esm/fileHead.js +27 -0
- package/dist/esm/formatters/eventHighlighter.js +332 -0
- package/dist/esm/formatters/noiseClassifier.js +209 -0
- package/dist/esm/formatters/sessionDump.js +328 -0
- package/dist/esm/formatters/toolSummary.js +226 -0
- package/dist/esm/formatting.js +82 -0
- package/dist/esm/handoffUrl.js +23 -0
- package/dist/esm/historicalStore.js +305 -0
- package/dist/esm/humanPrompt.js +88 -0
- package/dist/esm/index.js +213 -0
- package/dist/esm/keepAlive.js +189 -0
- package/dist/esm/liveCredentialGuard.js +33 -0
- package/dist/esm/modelAliases.js +51 -0
- package/dist/esm/modelCatalog.js +139 -0
- package/dist/esm/modelContext.js +280 -0
- package/dist/esm/modelInfo.js +832 -0
- package/dist/esm/multiProviderQuotaService.js +450 -0
- package/dist/esm/node.js +46 -0
- package/dist/esm/observedContextWindows.js +165 -0
- package/dist/esm/observedSessionCollector.js +939 -0
- package/dist/esm/parsers/changelogParser.js +71 -0
- package/dist/esm/parsers/codexHistory.js +89 -0
- package/dist/esm/parsers/codexParser.js +906 -0
- package/dist/esm/parsers/debugLogParser.js +126 -0
- package/dist/esm/parsers/jsonl.js +66 -0
- package/dist/esm/parsers/mcpToolName.js +28 -0
- package/dist/esm/parsers/openCodeParser.js +625 -0
- package/dist/esm/parsers/planExtractor.js +335 -0
- package/dist/esm/parsers/sessionActivityDetector.js +246 -0
- package/dist/esm/parsers/sessionPathResolver.js +713 -0
- package/dist/esm/parsers/subagentScanner.js +360 -0
- package/dist/esm/parsers/subagentStatsCache.js +45 -0
- package/dist/esm/parsers/subagentTraceParser.js +325 -0
- package/dist/esm/paths.js +134 -0
- package/dist/esm/peakHours.js +194 -0
- package/dist/esm/phrases.js +1330 -0
- package/dist/esm/pricingCatalog.js +247 -0
- package/dist/esm/processDetection.js +200 -0
- package/dist/esm/projectMigration.js +59 -0
- package/dist/esm/promptHistory.js +1221 -0
- package/dist/esm/providerFailure.js +362 -0
- package/dist/esm/providerIds.js +6 -0
- package/dist/esm/providerQuota.js +1 -0
- package/dist/esm/providerServiceStatus.js +145 -0
- package/dist/esm/providerServiceStatusTypes.js +1 -0
- package/dist/esm/providerStatus.js +117 -0
- package/dist/esm/providers/claudeCode.js +575 -0
- package/dist/esm/providers/claudeListing.js +121 -0
- package/dist/esm/providers/codex.js +1034 -0
- package/dist/esm/providers/codexDatabase.js +297 -0
- package/dist/esm/providers/codexRollouts.js +215 -0
- package/dist/esm/providers/detect.js +185 -0
- package/dist/esm/providers/diagnostics.js +83 -0
- package/dist/esm/providers/directoryListingCache.js +206 -0
- package/dist/esm/providers/factory.js +54 -0
- package/dist/esm/providers/openCode.js +2008 -0
- package/dist/esm/providers/openCodeDatabase.js +419 -0
- package/dist/esm/providers/rolloutWalker.js +252 -0
- package/dist/esm/providers/types.js +12 -0
- package/dist/esm/quota.js +182 -0
- package/dist/esm/quotaHistory.js +364 -0
- package/dist/esm/quotaPoller.js +230 -0
- package/dist/esm/quotaPresentation.js +97 -0
- package/dist/esm/quotaResolver.js +231 -0
- package/dist/esm/quotaSnapshots.js +130 -0
- package/dist/esm/quotaThresholds.js +85 -0
- package/dist/esm/readers/decisions.js +24 -0
- package/dist/esm/readers/handoff.js +20 -0
- package/dist/esm/readers/helpers.js +27 -0
- package/dist/esm/readers/history.js +9 -0
- package/dist/esm/readers/notes.js +48 -0
- package/dist/esm/readers/plans.js +225 -0
- package/dist/esm/readers/tasks.js +19 -0
- package/dist/esm/report/htmlHelpers.js +280 -0
- package/dist/esm/report/htmlReportGenerator.js +704 -0
- package/dist/esm/report/index.js +7 -0
- package/dist/esm/report/logo.js +2 -0
- package/dist/esm/report/openBrowser.js +19 -0
- package/dist/esm/report/sessionReportInputs.js +33 -0
- package/dist/esm/report/transcriptParser.js +83 -0
- package/dist/esm/report/types.js +4 -0
- package/dist/esm/schemas/accountManager.js +204 -0
- package/dist/esm/schemas/accountStatus.js +25 -0
- package/dist/esm/schemas/assistantTurn.js +74 -0
- package/dist/esm/schemas/index.js +25 -0
- package/dist/esm/schemas/observedSessionV1.js +77 -0
- package/dist/esm/schemas/providerFailure.js +37 -0
- package/dist/esm/schemas/quota.js +132 -0
- package/dist/esm/schemas/quotaHistory.js +40 -0
- package/dist/esm/schemas/sessionEvent.js +216 -0
- package/dist/esm/schemas/stateFile.js +75 -0
- package/dist/esm/schemas/tokenEstimation.js +7 -0
- package/dist/esm/schemas/transcript.js +86 -0
- package/dist/esm/schemas/usageNormalization.js +38 -0
- package/dist/esm/search/advancedFilter.js +197 -0
- package/dist/esm/search/sessionSearch.js +66 -0
- package/dist/esm/sessionFingerprint.js +40 -0
- package/dist/esm/sessionMonitor.js +198 -0
- package/dist/esm/sessionPreviews.js +522 -0
- package/dist/esm/sessionPromptHistory.js +254 -0
- package/dist/esm/sessionPromptSignals.js +364 -0
- package/dist/esm/sessionStats.js +160 -0
- package/dist/esm/sessionTokenTotals.js +98 -0
- package/dist/esm/sessionTranscripts.js +49 -0
- package/dist/esm/stateFile.js +119 -0
- package/dist/esm/statusline/BurnRateCalculator.js +40 -0
- package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
- package/dist/esm/statusline/formatter.js +122 -0
- package/dist/esm/statusline/index.js +8 -0
- package/dist/esm/terminalSync.js +95 -0
- package/dist/esm/testing/credentialGuard.js +197 -0
- package/dist/esm/testing/index.js +12 -0
- package/dist/esm/testing/sessionLogFixtures.js +128 -0
- package/dist/esm/tokenEstimation.js +67 -0
- package/dist/esm/tokenSummary.js +113 -0
- package/dist/esm/toolCategory.js +82 -0
- package/dist/esm/transcript.js +257 -0
- package/dist/esm/turns/assistantTurn.js +373 -0
- package/dist/esm/types/codex.js +23 -0
- package/dist/esm/types/decisionLog.js +5 -0
- package/dist/esm/types/historicalData.js +47 -0
- package/dist/esm/types/knowledgeNote.js +15 -0
- package/dist/esm/types/observedSessionV1.js +257 -0
- package/dist/esm/types/opencode.js +12 -0
- package/dist/esm/types/plan.js +7 -0
- package/dist/esm/types/sessionEvent.js +10 -0
- package/dist/esm/types/taskPersistence.js +12 -0
- package/dist/esm/usage/billingBlocks.js +148 -0
- package/dist/esm/usage/claudeUsageDedupe.js +189 -0
- package/dist/esm/usage/importSessionHistory.js +84 -0
- package/dist/esm/usage/subagentUsage.js +18 -0
- package/dist/esm/usage/usageEvents.js +402 -0
- package/dist/esm/usage/usageReports.js +202 -0
- package/dist/esm/usageNormalization.js +95 -0
- package/dist/esm/watchers/eventBridge.js +219 -0
- package/dist/esm/watchers/factory.js +63 -0
- package/dist/esm/watchers/jsonlTail.js +289 -0
- package/dist/esm/watchers/jsonlWatcher.js +435 -0
- package/dist/esm/watchers/providerReaderWatcher.js +118 -0
- package/dist/esm/watchers/sqliteWatcher.js +285 -0
- package/dist/esm/watchers/types.js +4 -0
- package/dist/esm/workspaceRoots.js +56 -0
- package/dist/esm/writers/atomic.js +375 -0
- package/dist/esm/writers/decisions.js +51 -0
- package/dist/esm/writers/notes.js +46 -0
- package/dist/esm/writers/tasks.js +63 -0
- package/dist/esm/zaiQuota.js +323 -0
- package/dist/esm/zaiQuotaApi.js +271 -0
- package/dist/esm/zaiQuotaWatcher.js +265 -0
- package/dist/fileHead.d.ts +22 -0
- package/dist/fileHead.js +33 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +49 -15
- package/dist/node.d.ts +13 -1
- package/dist/node.js +43 -2
- package/dist/parsers/codexHistory.js +2 -2
- package/dist/parsers/codexParser.d.ts +12 -0
- package/dist/parsers/codexParser.js +34 -12
- package/dist/parsers/debugLogParser.js +2 -2
- package/dist/parsers/sessionPathResolver.js +5 -4
- package/dist/promptHistory.d.ts +32 -0
- package/dist/promptHistory.js +115 -59
- package/dist/providerFailure.d.ts +12 -6
- package/dist/providerFailure.js +45 -0
- package/dist/providers/claudeCode.js +7 -99
- package/dist/providers/claudeListing.d.ts +22 -0
- package/dist/providers/claudeListing.js +158 -0
- package/dist/providers/codex.d.ts +3 -1
- package/dist/providers/codex.js +26 -227
- package/dist/providers/codexRollouts.d.ts +44 -0
- package/dist/providers/codexRollouts.js +261 -0
- package/dist/providers/detect.js +4 -4
- package/dist/readers/plans.js +3 -3
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/providerFailure.d.ts +154 -0
- package/dist/schemas/providerFailure.js +40 -0
- package/dist/sessionPromptHistory.d.ts +12 -7
- package/dist/sessionPromptHistory.js +15 -3
- package/dist/sessionPromptSignals.d.ts +9 -0
- package/dist/sessionPromptSignals.js +13 -0
- package/dist/testing/credentialGuard.d.ts +23 -0
- package/dist/testing/credentialGuard.js +233 -0
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.js +22 -0
- package/dist/testing/sessionLogFixtures.d.ts +28 -0
- package/dist/testing/sessionLogFixtures.js +169 -0
- package/dist/tokenEstimation.d.ts +2 -0
- package/dist/tokenEstimation.js +21 -11
- package/dist/toolCategory.d.ts +18 -0
- package/dist/toolCategory.js +86 -0
- package/dist/types/codex.d.ts +4 -0
- package/dist/usage/usageEvents.d.ts +16 -0
- package/dist/usage/usageEvents.js +53 -9
- package/dist/watchers/jsonlTail.d.ts +26 -1
- package/dist/watchers/jsonlTail.js +136 -28
- package/dist/watchers/jsonlWatcher.js +7 -0
- package/dist/watchers/providerReaderWatcher.js +1 -0
- package/dist/watchers/types.d.ts +6 -0
- package/dist/workspaceRoots.d.ts +23 -0
- package/dist/workspaceRoots.js +93 -0
- package/package.json +22 -2
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// ── Tier budgets ──
|
|
2
|
+
/**
|
|
3
|
+
* Published z.ai coding-plan prompt budgets.
|
|
4
|
+
*
|
|
5
|
+
* Source: https://docs.z.ai/devpack/overview & /devpack/faq.
|
|
6
|
+
* "One prompt ≈ one query; each prompt is estimated to invoke the model
|
|
7
|
+
* 15–20 times." We use the midpoint (17.5) when converting observed
|
|
8
|
+
* assistant turns into prompt equivalents.
|
|
9
|
+
*
|
|
10
|
+
* @deprecated Use `resolveZaiQuota()` from `zaiQuotaApi`, which reads z.ai's
|
|
11
|
+
* authoritative quota endpoint. This observed-traffic estimator is retained
|
|
12
|
+
* for backward compatibility only and drifts from actual billing.
|
|
13
|
+
*/
|
|
14
|
+
export const ZAI_TIER_BUDGETS = {
|
|
15
|
+
lite: { fiveHour: 80, weekly: 400 },
|
|
16
|
+
pro: { fiveHour: 400, weekly: 2000 },
|
|
17
|
+
max: { fiveHour: 1600, weekly: 8000 },
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Midpoint of z.ai's documented "15–20 model invocations per prompt" range.
|
|
21
|
+
* Used to convert accumulated assistant turns into prompt equivalents.
|
|
22
|
+
* Calibration note: this constant may need per-tier adjustment after
|
|
23
|
+
* real-world validation against the z.ai web UI.
|
|
24
|
+
*
|
|
25
|
+
* @deprecated Use `resolveZaiQuota()` from `zaiQuotaApi`, which reads z.ai's
|
|
26
|
+
* authoritative quota endpoint. This observed-traffic estimator is retained
|
|
27
|
+
* for backward compatibility only and drifts from actual billing.
|
|
28
|
+
*/
|
|
29
|
+
export const ZAI_PROMPT_INVOCATIONS = 17.5;
|
|
30
|
+
const FIVE_HOUR_MS = 5 * 3_600_000;
|
|
31
|
+
const SEVEN_DAY_MS = 7 * 86_400_000;
|
|
32
|
+
/**
|
|
33
|
+
* Provider IDs that OpenCode tags on z.ai-routed messages.
|
|
34
|
+
* `zai-coding-plan` is the Coding Plan key; `zai` is the bare API key.
|
|
35
|
+
*
|
|
36
|
+
* @deprecated Use `resolveZaiQuota()` from `zaiQuotaApi`, which reads z.ai's
|
|
37
|
+
* authoritative quota endpoint. This observed-traffic estimator is retained
|
|
38
|
+
* for backward compatibility only and drifts from actual billing.
|
|
39
|
+
*/
|
|
40
|
+
export const ZAI_PROVIDER_IDS = ['zai', 'zai-coding-plan'];
|
|
41
|
+
/**
|
|
42
|
+
* @deprecated Use `resolveZaiQuota()` from `zaiQuotaApi`, which reads z.ai's
|
|
43
|
+
* authoritative quota endpoint. This observed-traffic estimator is retained
|
|
44
|
+
* for backward compatibility only and drifts from actual billing.
|
|
45
|
+
*/
|
|
46
|
+
export function isZaiProviderId(value) {
|
|
47
|
+
return value === 'zai' || value === 'zai-coding-plan';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Sums the token fields of a single turn into a scalar.
|
|
51
|
+
* Cache reads are weighted at 0.1× (Anthropic convention) to avoid
|
|
52
|
+
* over-counting repeated context. Output and reasoning tokens are weighted 1×.
|
|
53
|
+
*
|
|
54
|
+
* @deprecated Use `resolveZaiQuota()` from `zaiQuotaApi`, which reads z.ai's
|
|
55
|
+
* authoritative quota endpoint. This observed-traffic estimator is retained
|
|
56
|
+
* for backward compatibility only and drifts from actual billing.
|
|
57
|
+
*/
|
|
58
|
+
export function turnTokenWeight(turn) {
|
|
59
|
+
return (turn.inputTokens +
|
|
60
|
+
turn.outputTokens +
|
|
61
|
+
Math.round(turn.cacheReadTokens * 0.1) +
|
|
62
|
+
turn.cacheWriteTokens +
|
|
63
|
+
(turn.reasoningTokens || 0));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Reduces a stream of z.ai-routed assistant turns into rolling-window usage.
|
|
67
|
+
*
|
|
68
|
+
* Pure & stateless: the caller is responsible for passing the relevant turns.
|
|
69
|
+
* Turns outside the 7-day window are ignored; turns inside the 7-day window
|
|
70
|
+
* but outside the 5-hour window still count toward weekly usage.
|
|
71
|
+
*
|
|
72
|
+
* @deprecated Use `resolveZaiQuota()` from `zaiQuotaApi`, which reads z.ai's
|
|
73
|
+
* authoritative quota endpoint. This observed-traffic estimator is retained
|
|
74
|
+
* for backward compatibility only and drifts from actual billing.
|
|
75
|
+
*/
|
|
76
|
+
export function accumulateZaiUsage(turns, nowMs = Date.now()) {
|
|
77
|
+
const fiveHourCutoff = nowMs - FIVE_HOUR_MS;
|
|
78
|
+
const weeklyCutoff = nowMs - SEVEN_DAY_MS;
|
|
79
|
+
let fiveHourTurns = 0;
|
|
80
|
+
let weeklyTurns = 0;
|
|
81
|
+
let fiveHourTokens = 0;
|
|
82
|
+
let weeklyTokens = 0;
|
|
83
|
+
let fiveHourStartedAtMs = null;
|
|
84
|
+
let weeklyStartedAtMs = null;
|
|
85
|
+
for (const turn of turns) {
|
|
86
|
+
if (turn.timestampMs < weeklyCutoff)
|
|
87
|
+
continue;
|
|
88
|
+
weeklyTurns += 1;
|
|
89
|
+
weeklyTokens += turnTokenWeight(turn);
|
|
90
|
+
if (weeklyStartedAtMs === null || turn.timestampMs < weeklyStartedAtMs) {
|
|
91
|
+
weeklyStartedAtMs = turn.timestampMs;
|
|
92
|
+
}
|
|
93
|
+
if (turn.timestampMs >= fiveHourCutoff) {
|
|
94
|
+
fiveHourTurns += 1;
|
|
95
|
+
fiveHourTokens += turnTokenWeight(turn);
|
|
96
|
+
if (fiveHourStartedAtMs === null || turn.timestampMs < fiveHourStartedAtMs) {
|
|
97
|
+
fiveHourStartedAtMs = turn.timestampMs;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
fiveHourTurns,
|
|
103
|
+
weeklyTurns,
|
|
104
|
+
fiveHourTokens,
|
|
105
|
+
weeklyTokens,
|
|
106
|
+
fiveHourPrompts: fiveHourTurns / ZAI_PROMPT_INVOCATIONS,
|
|
107
|
+
weeklyPrompts: weeklyTurns / ZAI_PROMPT_INVOCATIONS,
|
|
108
|
+
fiveHourStartedAtMs,
|
|
109
|
+
weeklyStartedAtMs,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// ── Tier resolution ──
|
|
113
|
+
/**
|
|
114
|
+
* Resolves the effective z.ai tier when the user has selected `'auto'`.
|
|
115
|
+
*
|
|
116
|
+
* Heuristic: if accumulated weekly prompts already exceed a tier's budget,
|
|
117
|
+
* the user must be on a higher tier. This under-detects at the start of a
|
|
118
|
+
* weekly cycle (a Max user looks identical to a Lite user until they cross
|
|
119
|
+
* 400 prompts/week), so callers should still expose an explicit override.
|
|
120
|
+
*
|
|
121
|
+
* @deprecated Use `resolveZaiQuota()` from `zaiQuotaApi`, which reads z.ai's
|
|
122
|
+
* authoritative quota endpoint. This observed-traffic estimator is retained
|
|
123
|
+
* for backward compatibility only and drifts from actual billing.
|
|
124
|
+
*/
|
|
125
|
+
export function resolveZaiTier(configured, accumulated) {
|
|
126
|
+
if (configured !== 'auto')
|
|
127
|
+
return configured;
|
|
128
|
+
if (accumulated.weeklyPrompts > ZAI_TIER_BUDGETS.pro.weekly)
|
|
129
|
+
return 'max';
|
|
130
|
+
if (accumulated.weeklyPrompts > ZAI_TIER_BUDGETS.lite.weekly)
|
|
131
|
+
return 'pro';
|
|
132
|
+
return 'lite';
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Converts accumulated usage into a shared `QuotaState`.
|
|
136
|
+
*
|
|
137
|
+
* The two windows reuse the `fiveHour` / `sevenDay` field names for cross-
|
|
138
|
+
* provider compatibility, but are labeled "5-Hour" and "Weekly" via
|
|
139
|
+
* `fiveHourLabel` / `sevenDayLabel` — mirroring how Codex relabels its
|
|
140
|
+
* generic primary/secondary windows.
|
|
141
|
+
*
|
|
142
|
+
* Reset timestamps are best-effort because z.ai does not expose them:
|
|
143
|
+
* - `fiveHour.resetsAt` ← first turn in window + 5h, unless an error
|
|
144
|
+
* event supplied an authoritative reset.
|
|
145
|
+
* - `sevenDay.resetsAt` ← first turn in window + 7d (documented as
|
|
146
|
+
* "7 days from order time" — order time is unknown, so this is a
|
|
147
|
+
* lower bound at best).
|
|
148
|
+
*
|
|
149
|
+
* @deprecated Use `resolveZaiQuota()` from `zaiQuotaApi`, which reads z.ai's
|
|
150
|
+
* authoritative quota endpoint. This observed-traffic estimator is retained
|
|
151
|
+
* for backward compatibility only and drifts from actual billing.
|
|
152
|
+
*/
|
|
153
|
+
export function inferZaiQuotaState(accumulated, tier, options = {}) {
|
|
154
|
+
const budget = ZAI_TIER_BUDGETS[tier];
|
|
155
|
+
const capturedAt = options.capturedAt ?? new Date().toISOString();
|
|
156
|
+
const fiveHourUtil = budget.fiveHour > 0 ? Math.min((accumulated.fiveHourPrompts / budget.fiveHour) * 100, 200) : 0;
|
|
157
|
+
const weeklyUtil = budget.weekly > 0 ? Math.min((accumulated.weeklyPrompts / budget.weekly) * 100, 200) : 0;
|
|
158
|
+
const fiveHourResetsAt = options.authoritativeFiveHourResetAt ??
|
|
159
|
+
(accumulated.fiveHourStartedAtMs !== null
|
|
160
|
+
? new Date(accumulated.fiveHourStartedAtMs + FIVE_HOUR_MS).toISOString()
|
|
161
|
+
: '');
|
|
162
|
+
const sevenDayResetsAt = options.authoritativeWeeklyResetAt ??
|
|
163
|
+
(accumulated.weeklyStartedAtMs !== null
|
|
164
|
+
? new Date(accumulated.weeklyStartedAtMs + SEVEN_DAY_MS).toISOString()
|
|
165
|
+
: '');
|
|
166
|
+
const fiveHour = {
|
|
167
|
+
utilization: Math.round(fiveHourUtil * 10) / 10,
|
|
168
|
+
resetsAt: fiveHourResetsAt,
|
|
169
|
+
};
|
|
170
|
+
const sevenDay = {
|
|
171
|
+
utilization: Math.round(weeklyUtil * 10) / 10,
|
|
172
|
+
resetsAt: sevenDayResetsAt,
|
|
173
|
+
};
|
|
174
|
+
const hasAnyData = accumulated.fiveHourTurns > 0 || accumulated.weeklyTurns > 0;
|
|
175
|
+
return {
|
|
176
|
+
fiveHour,
|
|
177
|
+
sevenDay,
|
|
178
|
+
available: hasAnyData,
|
|
179
|
+
providerId: 'zai',
|
|
180
|
+
source: 'session',
|
|
181
|
+
capturedAt,
|
|
182
|
+
stale: options.stale ?? !hasAnyData,
|
|
183
|
+
fiveHourLabel: '5-Hour',
|
|
184
|
+
sevenDayLabel: 'Weekly',
|
|
185
|
+
planType: tier,
|
|
186
|
+
limitId: `zai-${tier}`,
|
|
187
|
+
limitName: `z.ai ${tier.charAt(0).toUpperCase() + tier.slice(1)}`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* z.ai business error codes that signal quota/plan state.
|
|
192
|
+
* Source: https://docs.z.ai/api-reference/api-code
|
|
193
|
+
*/
|
|
194
|
+
const ZAI_ERROR_CODES = {
|
|
195
|
+
'1308': 'exhausted', // Usage limit reached for `{number} {unit}` (includes next_flush_time)
|
|
196
|
+
'1310': 'exhausted', // Weekly/Monthly limit exhausted (resets at next_flush_time)
|
|
197
|
+
'1313': 'fup', // Fair-Use-Policy violation — request rate restricted
|
|
198
|
+
'1309': 'expired', // GLM Coding Plan package has expired
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Extracts a z.ai quota error from a message-error payload, if present.
|
|
202
|
+
*
|
|
203
|
+
* The reset timestamp is embedded inside the human-readable `message` text
|
|
204
|
+
* as `${next_flush_time}`. z.ai's docs interpolate the value into the
|
|
205
|
+
* message string; we parse ISO datetimes, epoch seconds, and the literal
|
|
206
|
+
* `YYYY-MM-DD HH:mm:ss` form defensively.
|
|
207
|
+
*
|
|
208
|
+
* Returns `null` when the code is not a quota-related business error.
|
|
209
|
+
*
|
|
210
|
+
* @deprecated Use `resolveZaiQuota()` from `zaiQuotaApi`, which reads z.ai's
|
|
211
|
+
* authoritative quota endpoint. This observed-traffic estimator is retained
|
|
212
|
+
* for backward compatibility only and drifts from actual billing.
|
|
213
|
+
*/
|
|
214
|
+
export function parseZaiQuotaError(error) {
|
|
215
|
+
if (!error)
|
|
216
|
+
return null;
|
|
217
|
+
const code = String(error.code ?? '');
|
|
218
|
+
const kind = ZAI_ERROR_CODES[code];
|
|
219
|
+
if (!kind)
|
|
220
|
+
return null;
|
|
221
|
+
const message = error.message ?? '';
|
|
222
|
+
const resetsAt = extractNextFlushTime(message);
|
|
223
|
+
return { kind, resetsAt, code, message };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Parses `${next_flush_time}` out of a z.ai error message.
|
|
227
|
+
*
|
|
228
|
+
* Handles ISO 8601 (`2025-01-31T14:00:00Z`), space-separated
|
|
229
|
+
* (`2025-01-31 14:00:00`), and epoch-seconds (1234567890) forms.
|
|
230
|
+
* Returns undefined when no parseable timestamp is found.
|
|
231
|
+
*
|
|
232
|
+
* @deprecated Use `resolveZaiQuota()` from `zaiQuotaApi`, which reads z.ai's
|
|
233
|
+
* authoritative quota endpoint. This observed-traffic estimator is retained
|
|
234
|
+
* for backward compatibility only and drifts from actual billing.
|
|
235
|
+
*/
|
|
236
|
+
export function extractNextFlushTime(message) {
|
|
237
|
+
if (!message)
|
|
238
|
+
return undefined;
|
|
239
|
+
// ISO 8601 with optional timezone.
|
|
240
|
+
const isoMatch = message.match(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)/);
|
|
241
|
+
if (isoMatch) {
|
|
242
|
+
const ms = Date.parse(isoMatch[1]);
|
|
243
|
+
if (Number.isFinite(ms))
|
|
244
|
+
return new Date(ms).toISOString();
|
|
245
|
+
}
|
|
246
|
+
// `YYYY-MM-DD HH:mm:ss` (no T separator) — z.ai's likely interpolated form.
|
|
247
|
+
const spaceMatch = message.match(/(\d{4}-\d{2}-\d{2}[ ]\d{2}:\d{2}:\d{2})/);
|
|
248
|
+
if (spaceMatch) {
|
|
249
|
+
const normalised = spaceMatch[1].replace(' ', 'T');
|
|
250
|
+
const ms = Date.parse(`${normalised}Z`);
|
|
251
|
+
if (Number.isFinite(ms))
|
|
252
|
+
return new Date(ms).toISOString();
|
|
253
|
+
}
|
|
254
|
+
// Epoch seconds — 10-digit integer in a plausible range.
|
|
255
|
+
const epochMatch = message.match(/(?<!\d)(\d{10})(?!\d)/);
|
|
256
|
+
if (epochMatch) {
|
|
257
|
+
const seconds = Number(epochMatch[1]);
|
|
258
|
+
if (seconds > 1_700_000_000 && seconds < 2_000_000_000) {
|
|
259
|
+
return new Date(seconds * 1000).toISOString();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
// ── Unavailable state helper ──
|
|
265
|
+
/**
|
|
266
|
+
* Builds an unavailable z.ai `QuotaState` for the cold-start case where
|
|
267
|
+
* no z.ai traffic has been observed yet.
|
|
268
|
+
*
|
|
269
|
+
* @deprecated Use `resolveZaiQuota()` from `zaiQuotaApi`, which reads z.ai's
|
|
270
|
+
* authoritative quota endpoint. This observed-traffic estimator is retained
|
|
271
|
+
* for backward compatibility only and drifts from actual billing.
|
|
272
|
+
*/
|
|
273
|
+
export function makeUnavailableZaiQuotaState(error = 'No z.ai usage observed yet', tier = 'lite', capturedAt = new Date().toISOString()) {
|
|
274
|
+
return {
|
|
275
|
+
fiveHour: { utilization: 0, resetsAt: '' },
|
|
276
|
+
sevenDay: { utilization: 0, resetsAt: '' },
|
|
277
|
+
available: false,
|
|
278
|
+
error,
|
|
279
|
+
providerId: 'zai',
|
|
280
|
+
source: 'session',
|
|
281
|
+
capturedAt,
|
|
282
|
+
stale: true,
|
|
283
|
+
fiveHourLabel: '5-Hour',
|
|
284
|
+
sevenDayLabel: 'Weekly',
|
|
285
|
+
planType: tier,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Converts raw OpenCode DB rows (filtered to z.ai providerIDs by the caller)
|
|
290
|
+
* into the watcher's `ZaiAssistantTurn` shape, and surfaces any trapped
|
|
291
|
+
* business errors.
|
|
292
|
+
*
|
|
293
|
+
* Returns `{ turns, errors }` so callers can replay them through
|
|
294
|
+
* `ZaiQuotaWatcher.ingestAssistantTurns` and `ingestError`.
|
|
295
|
+
*
|
|
296
|
+
* @deprecated Use `resolveZaiQuota()` from `zaiQuotaApi`, which reads z.ai's
|
|
297
|
+
* authoritative quota endpoint. This observed-traffic estimator is retained
|
|
298
|
+
* for backward compatibility only and drifts from actual billing.
|
|
299
|
+
*/
|
|
300
|
+
export function rowsToZaiTurnsAndErrors(rows) {
|
|
301
|
+
const turns = [];
|
|
302
|
+
const errors = [];
|
|
303
|
+
for (const row of rows) {
|
|
304
|
+
turns.push({
|
|
305
|
+
timestampMs: row.timeCreated,
|
|
306
|
+
model: row.modelId,
|
|
307
|
+
inputTokens: row.inputTokens || 0,
|
|
308
|
+
outputTokens: row.outputTokens || 0,
|
|
309
|
+
cacheReadTokens: row.cacheReadTokens || 0,
|
|
310
|
+
cacheWriteTokens: row.cacheWriteTokens || 0,
|
|
311
|
+
reasoningTokens: row.reasoningTokens || 0,
|
|
312
|
+
});
|
|
313
|
+
if (row.errorCode != null || row.errorMessage) {
|
|
314
|
+
const parsed = parseZaiQuotaError({
|
|
315
|
+
code: row.errorCode ?? undefined,
|
|
316
|
+
message: row.errorMessage ?? undefined,
|
|
317
|
+
});
|
|
318
|
+
if (parsed)
|
|
319
|
+
errors.push(parsed);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { turns, errors };
|
|
323
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { readQuotaSnapshot, writeQuotaSnapshot } from './quotaSnapshots';
|
|
5
|
+
import { FIVE_HOUR_WINDOW_MS, SEVEN_DAY_WINDOW_MS, withQuotaProjections } from './quota';
|
|
6
|
+
const DEFAULT_ZAI_BASE_URL = 'https://api.z.ai/api/anthropic';
|
|
7
|
+
const QUOTA_PATH = '/api/monitor/usage/quota/limit';
|
|
8
|
+
const DEFAULT_ACCOUNT_ID = 'default';
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
10
|
+
function unavailableZaiQuotaState(error, meta = {}, capturedAt = new Date().toISOString()) {
|
|
11
|
+
return {
|
|
12
|
+
fiveHour: { utilization: 0, resetsAt: '' },
|
|
13
|
+
sevenDay: { utilization: 0, resetsAt: '' },
|
|
14
|
+
available: false,
|
|
15
|
+
error,
|
|
16
|
+
providerId: 'zai',
|
|
17
|
+
source: 'api',
|
|
18
|
+
capturedAt,
|
|
19
|
+
fiveHourLabel: '5-Hour',
|
|
20
|
+
sevenDayLabel: 'Weekly',
|
|
21
|
+
...meta,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function openCodeDataDirCandidates() {
|
|
25
|
+
const candidates = [];
|
|
26
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
27
|
+
if (xdg)
|
|
28
|
+
candidates.push(path.join(xdg, 'opencode'));
|
|
29
|
+
candidates.push(path.join(os.homedir(), '.local', 'share', 'opencode'));
|
|
30
|
+
if (process.platform === 'darwin') {
|
|
31
|
+
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'opencode'));
|
|
32
|
+
}
|
|
33
|
+
else if (process.platform === 'win32') {
|
|
34
|
+
candidates.push(path.join(process.env.LOCALAPPDATA ||
|
|
35
|
+
process.env.APPDATA ||
|
|
36
|
+
path.join(os.homedir(), 'AppData', 'Local'), 'opencode'));
|
|
37
|
+
}
|
|
38
|
+
return Array.from(new Set(candidates));
|
|
39
|
+
}
|
|
40
|
+
function readOpenCodeAuthToken(openCodeDataDir) {
|
|
41
|
+
const candidates = openCodeDataDir ? [openCodeDataDir] : openCodeDataDirCandidates();
|
|
42
|
+
for (const dataDir of candidates) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dataDir, 'auth.json'), 'utf8'));
|
|
45
|
+
const codingPlan = parsed['zai-coding-plan'];
|
|
46
|
+
const zai = parsed.zai;
|
|
47
|
+
if (typeof codingPlan?.key === 'string' && codingPlan.key.trim())
|
|
48
|
+
return codingPlan.key.trim();
|
|
49
|
+
if (typeof zai?.key === 'string' && zai.key.trim())
|
|
50
|
+
return zai.key.trim();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Try the next candidate.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function platformFromBaseUrl(baseUrl) {
|
|
59
|
+
if (baseUrl.includes('api.z.ai'))
|
|
60
|
+
return 'ZAI';
|
|
61
|
+
if (baseUrl.includes('open.bigmodel.cn') || baseUrl.includes('dev.bigmodel.cn'))
|
|
62
|
+
return 'ZHIPU';
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
export function readZaiCredentials(options = {}) {
|
|
66
|
+
const openCodeToken = readOpenCodeAuthToken(options.openCodeDataDir);
|
|
67
|
+
if (openCodeToken) {
|
|
68
|
+
return {
|
|
69
|
+
authToken: openCodeToken,
|
|
70
|
+
baseUrl: DEFAULT_ZAI_BASE_URL,
|
|
71
|
+
platform: 'ZAI',
|
|
72
|
+
source: 'opencode',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const env = options.env ?? process.env;
|
|
76
|
+
const authToken = env.ANTHROPIC_AUTH_TOKEN?.trim();
|
|
77
|
+
const baseUrl = env.ANTHROPIC_BASE_URL?.trim();
|
|
78
|
+
if (!authToken || !baseUrl)
|
|
79
|
+
return null;
|
|
80
|
+
const platform = platformFromBaseUrl(baseUrl);
|
|
81
|
+
if (!platform)
|
|
82
|
+
return null;
|
|
83
|
+
return {
|
|
84
|
+
authToken,
|
|
85
|
+
baseUrl,
|
|
86
|
+
platform,
|
|
87
|
+
source: 'env',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function quotaLimitUrl(baseUrl) {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(baseUrl);
|
|
93
|
+
return `${parsed.protocol}//${parsed.host}${QUOTA_PATH}`;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function tokenLimitEntries(payload) {
|
|
100
|
+
const limits = payload.data?.limits ?? payload.limits ?? [];
|
|
101
|
+
return limits.filter((item) => item?.type === 'TOKENS_LIMIT' && typeof item.percentage === 'number');
|
|
102
|
+
}
|
|
103
|
+
function isFiveHourLimit(item) {
|
|
104
|
+
return item.type === 'TOKENS_LIMIT' && item.unit === 3 && item.number === 5;
|
|
105
|
+
}
|
|
106
|
+
function isWeeklyLimit(item) {
|
|
107
|
+
return item.type === 'TOKENS_LIMIT' && item.unit === 6 && item.number === 1;
|
|
108
|
+
}
|
|
109
|
+
function isoFromEpochMs(value) {
|
|
110
|
+
if (value == null)
|
|
111
|
+
return '';
|
|
112
|
+
const ms = typeof value === 'number' ? value : Number(value);
|
|
113
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
114
|
+
return '';
|
|
115
|
+
return new Date(ms).toISOString();
|
|
116
|
+
}
|
|
117
|
+
function displayPlanName(level) {
|
|
118
|
+
if (!level)
|
|
119
|
+
return 'z.ai Coding Plan';
|
|
120
|
+
return `z.ai ${level.charAt(0).toUpperCase()}${level.slice(1)}`;
|
|
121
|
+
}
|
|
122
|
+
export function quotaStateFromZaiQuotaLimitPayload(payload, capturedAt = new Date().toISOString()) {
|
|
123
|
+
const data = payload;
|
|
124
|
+
const tokenLimits = tokenLimitEntries(data);
|
|
125
|
+
const fiveHourLimit = tokenLimits.find(isFiveHourLimit) ?? tokenLimits[0];
|
|
126
|
+
const weeklyLimit = tokenLimits.find(isWeeklyLimit) ?? tokenLimits.find((item) => item !== fiveHourLimit);
|
|
127
|
+
const level = data.data?.level;
|
|
128
|
+
if (!fiveHourLimit || !weeklyLimit) {
|
|
129
|
+
return unavailableZaiQuotaState('z.ai quota API returned no token quota windows.', {
|
|
130
|
+
failureKind: 'unknown',
|
|
131
|
+
}, capturedAt);
|
|
132
|
+
}
|
|
133
|
+
return withQuotaProjections({
|
|
134
|
+
fiveHour: {
|
|
135
|
+
utilization: fiveHourLimit.percentage ?? 0,
|
|
136
|
+
resetsAt: isoFromEpochMs(fiveHourLimit.nextResetTime),
|
|
137
|
+
},
|
|
138
|
+
sevenDay: {
|
|
139
|
+
utilization: weeklyLimit.percentage ?? 0,
|
|
140
|
+
resetsAt: isoFromEpochMs(weeklyLimit.nextResetTime),
|
|
141
|
+
},
|
|
142
|
+
available: true,
|
|
143
|
+
providerId: 'zai',
|
|
144
|
+
source: 'api',
|
|
145
|
+
capturedAt,
|
|
146
|
+
fiveHourLabel: '5-Hour',
|
|
147
|
+
sevenDayLabel: 'Weekly',
|
|
148
|
+
planType: level,
|
|
149
|
+
limitId: level ? `zai-${level}` : 'zai-coding-plan',
|
|
150
|
+
limitName: displayPlanName(level),
|
|
151
|
+
}, {
|
|
152
|
+
fiveHourWindowMs: FIVE_HOUR_WINDOW_MS,
|
|
153
|
+
sevenDayWindowMs: SEVEN_DAY_WINDOW_MS,
|
|
154
|
+
capturedAt,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function parseRetryAfterMs(retryAfter) {
|
|
158
|
+
if (!retryAfter)
|
|
159
|
+
return undefined;
|
|
160
|
+
const seconds = Number(retryAfter);
|
|
161
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
162
|
+
return seconds * 1000;
|
|
163
|
+
const dateMs = Date.parse(retryAfter);
|
|
164
|
+
if (Number.isFinite(dateMs))
|
|
165
|
+
return Math.max(0, dateMs - Date.now());
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
function failureKindForStatus(status) {
|
|
169
|
+
if (status === 401 || status === 403)
|
|
170
|
+
return 'auth';
|
|
171
|
+
if (status === 429)
|
|
172
|
+
return 'rate_limit';
|
|
173
|
+
if (status >= 500)
|
|
174
|
+
return 'server';
|
|
175
|
+
return 'unknown';
|
|
176
|
+
}
|
|
177
|
+
function sanitizeErrorMessage(message, authToken) {
|
|
178
|
+
let result = message;
|
|
179
|
+
if (authToken) {
|
|
180
|
+
result = result.split(authToken).join('<redacted>');
|
|
181
|
+
}
|
|
182
|
+
return result.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/g, 'Bearer <redacted>');
|
|
183
|
+
}
|
|
184
|
+
export async function fetchZaiQuotaFromApi(options = {}) {
|
|
185
|
+
const capturedAt = options.capturedAt ?? new Date().toISOString();
|
|
186
|
+
const credentials = options.credentials ?? readZaiCredentials(options);
|
|
187
|
+
if (!credentials) {
|
|
188
|
+
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);
|
|
189
|
+
}
|
|
190
|
+
const url = quotaLimitUrl(credentials.baseUrl);
|
|
191
|
+
if (!url) {
|
|
192
|
+
return unavailableZaiQuotaState('Invalid z.ai base URL.', { failureKind: 'unknown' }, capturedAt);
|
|
193
|
+
}
|
|
194
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
195
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
198
|
+
timeout.unref?.();
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetchImpl(url, {
|
|
201
|
+
method: 'GET',
|
|
202
|
+
headers: {
|
|
203
|
+
Authorization: credentials.authToken,
|
|
204
|
+
'Accept-Language': 'en-US,en',
|
|
205
|
+
Accept: 'application/json',
|
|
206
|
+
'Content-Type': 'application/json',
|
|
207
|
+
},
|
|
208
|
+
signal: controller.signal,
|
|
209
|
+
});
|
|
210
|
+
const body = await response.text();
|
|
211
|
+
let parsed;
|
|
212
|
+
try {
|
|
213
|
+
parsed = JSON.parse(body);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
parsed = null;
|
|
217
|
+
}
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
const failureKind = failureKindForStatus(response.status);
|
|
220
|
+
const retryAfterMs = parseRetryAfterMs(response.headers.get('retry-after'));
|
|
221
|
+
const detail = parsed && typeof parsed === 'object' && 'msg' in parsed
|
|
222
|
+
? String(parsed.msg ?? '')
|
|
223
|
+
: '';
|
|
224
|
+
const baseMessage = failureKind === 'auth'
|
|
225
|
+
? `z.ai quota API rejected credentials (HTTP ${response.status}).`
|
|
226
|
+
: `z.ai quota API error (HTTP ${response.status}).`;
|
|
227
|
+
return unavailableZaiQuotaState(sanitizeErrorMessage([baseMessage, detail].filter(Boolean).join(' '), credentials.authToken), { failureKind, httpStatus: response.status, retryAfterMs }, capturedAt);
|
|
228
|
+
}
|
|
229
|
+
return quotaStateFromZaiQuotaLimitPayload(parsed, capturedAt);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
const isAbort = error instanceof Error && error.name === 'AbortError';
|
|
233
|
+
return unavailableZaiQuotaState(isAbort ? 'z.ai quota API timed out.' : 'z.ai quota API network error.', { failureKind: 'network' }, capturedAt);
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
clearTimeout(timeout);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Stamp a z.ai quota sample with its runtime provider, projections, and window labels. */
|
|
240
|
+
export function enrichZaiQuota(state) {
|
|
241
|
+
const withProjections = withQuotaProjections(state);
|
|
242
|
+
return {
|
|
243
|
+
...withProjections,
|
|
244
|
+
runtimeProvider: 'zai',
|
|
245
|
+
providerId: 'zai',
|
|
246
|
+
fiveHourLabel: state.fiveHourLabel ?? '5-Hour',
|
|
247
|
+
sevenDayLabel: state.sevenDayLabel ?? 'Weekly',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
export async function resolveZaiQuota(options = {}) {
|
|
251
|
+
const accountId = options.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
252
|
+
const readSnapshot = options.readSnapshot ?? readQuotaSnapshot;
|
|
253
|
+
const writeSnapshot = options.writeSnapshot ?? writeQuotaSnapshot;
|
|
254
|
+
const apiQuota = await fetchZaiQuotaFromApi(options);
|
|
255
|
+
if (apiQuota.available) {
|
|
256
|
+
writeSnapshot('zai', accountId, apiQuota);
|
|
257
|
+
return enrichZaiQuota(apiQuota);
|
|
258
|
+
}
|
|
259
|
+
const cached = readSnapshot('zai', accountId);
|
|
260
|
+
if (cached) {
|
|
261
|
+
return enrichZaiQuota({
|
|
262
|
+
...cached,
|
|
263
|
+
providerId: 'zai',
|
|
264
|
+
source: 'cache',
|
|
265
|
+
stale: true,
|
|
266
|
+
fiveHourLabel: cached.fiveHourLabel ?? '5-Hour',
|
|
267
|
+
sevenDayLabel: cached.sevenDayLabel ?? 'Weekly',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return enrichZaiQuota(apiQuota);
|
|
271
|
+
}
|