@tokentop/ttop 0.4.0 → 0.6.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/README.md +7 -6
- package/package.json +12 -5
- package/src/agents/aggregator.test.ts +306 -0
- package/src/agents/aggregator.ts +44 -2
- package/src/agents/types.ts +3 -0
- package/src/cli.ts +16 -2
- package/src/demo/simulator.ts +2 -2
- package/src/plugins/agents/index.ts +3 -0
- package/src/plugins/auth-sources.ts +2 -3
- package/src/plugins/lifecycle.ts +2 -2
- package/src/plugins/loader.test.ts +1 -1
- package/src/plugins/loader.ts +15 -7
- package/src/plugins/notification-bus.ts +5 -2
- package/src/plugins/notifications/terminal-bell.ts +1 -1
- package/src/plugins/notifications/visual-flash.ts +1 -1
- package/src/plugins/plugin-context-factory.ts +4 -19
- package/src/plugins/plugin-host.test.ts +197 -0
- package/src/plugins/plugin-host.ts +45 -1
- package/src/plugins/providers/anthropic.test.ts +202 -0
- package/src/plugins/providers/anthropic.ts +21 -2
- package/src/plugins/providers/antigravity.ts +17 -1
- package/src/plugins/providers/chutes.ts +205 -0
- package/src/plugins/providers/codex.ts +17 -1
- package/src/plugins/providers/gemini.ts +17 -1
- package/src/plugins/providers/github-copilot.ts +17 -1
- package/src/plugins/providers/index.ts +1 -0
- package/src/plugins/providers/minimax.ts +17 -1
- package/src/plugins/providers/openai-api.ts +53 -1
- package/src/plugins/providers/opencode-zen.ts +1 -1
- package/src/plugins/providers/perplexity.ts +11 -2
- package/src/plugins/providers/zai.ts +17 -1
- package/src/plugins/registry.ts +9 -9
- package/src/plugins/sandbox-guard.test.ts +1 -1
- package/src/plugins/sandbox-guard.ts +1 -1
- package/src/plugins/sandbox.ts +1 -1
- package/src/plugins/themes/catppuccin-latte.ts +1 -1
- package/src/plugins/themes/catppuccin-mocha.ts +1 -1
- package/src/plugins/themes/claude-code.ts +1 -1
- package/src/plugins/themes/dracula.ts +1 -1
- package/src/plugins/themes/github-light.ts +1 -1
- package/src/plugins/themes/gruvbox-dark.ts +1 -1
- package/src/plugins/themes/gruvbox-light.ts +1 -1
- package/src/plugins/themes/kanagawa.ts +1 -1
- package/src/plugins/themes/nord.ts +1 -1
- package/src/plugins/themes/one-dark.ts +1 -1
- package/src/plugins/themes/opencode.ts +1 -1
- package/src/plugins/themes/rose-pine-dawn.ts +1 -1
- package/src/plugins/themes/rose-pine.ts +1 -1
- package/src/plugins/themes/solarized-light.ts +1 -1
- package/src/plugins/themes/tokyo-night.ts +1 -1
- package/src/plugins/update-checker.ts +2 -2
- package/src/pricing/estimator.test.ts +1 -1
- package/src/pricing/estimator.ts +1 -1
- package/src/pricing/fallback.ts +5 -1
- package/src/pricing/index.ts +1 -1
- package/src/pricing/models-dev.ts +1 -1
- package/src/storage/index.ts +6 -0
- package/src/storage/migrations/index.ts +17 -0
- package/src/storage/repos/pluginStorage.ts +35 -0
- package/src/tui/App.tsx +17 -9
- package/src/tui/components/HelpOverlay.tsx +1 -1
- package/src/tui/components/LimitGauge.tsx +12 -4
- package/src/tui/components/ProviderCard.tsx +39 -23
- package/src/tui/components/ProviderDetailPanel.tsx +1 -1
- package/src/tui/components/ProviderLimitsPanel.tsx +5 -0
- package/src/tui/components/SessionDetailsDrawer.tsx +10 -0
- package/src/tui/components/SessionsTable.tsx +62 -37
- package/src/tui/components/SettingsModal.tsx +1 -1
- package/src/tui/components/Skeleton.tsx +9 -6
- package/src/tui/components/SortOverlay.tsx +167 -0
- package/src/tui/components/Spinner.tsx +16 -11
- package/src/tui/components/ThemePicker.tsx +1 -1
- package/src/tui/contexts/AgentSessionContext.tsx +70 -17
- package/src/tui/contexts/PluginContext.tsx +72 -14
- package/src/tui/contexts/RealTimeActivityContext.tsx +33 -20
- package/src/tui/contexts/ThemeContext.tsx +7 -4
- package/src/tui/createApp.tsx +2 -0
- package/src/tui/hooks/index.ts +1 -0
- package/src/tui/hooks/useAnimatedValue.ts +29 -26
- package/src/tui/hooks/useAnimationTick.ts +93 -0
- package/src/tui/hooks/useDashboardKeyboard.ts +11 -4
- package/src/tui/hooks/useEntranceAnimation.ts +16 -10
- package/src/tui/hooks/usePulse.ts +13 -7
- package/src/tui/hooks/useSessionEnrichedProviders.ts +1 -1
- package/src/tui/hooks/useValueFlash.ts +25 -33
- package/src/tui/index.tsx +4 -0
- package/src/tui/types/projectSort.ts +35 -0
- package/src/tui/types/sort.ts +61 -0
- package/src/tui/views/ProjectsView.tsx +104 -38
- package/src/tui/views/RealTimeDashboard.tsx +91 -8
- package/src/tui/views/SettingsView.tsx +1 -1
- package/src/utils/__tests__/error-category.test.ts +147 -0
- package/src/utils/currency.ts +4 -2
- package/src/utils/error-category.ts +91 -0
- package/src/version.ts +1 -1
- package/src/plugins/types/agent.ts +0 -108
- package/src/plugins/types/base.ts +0 -278
- package/src/plugins/types/index.ts +0 -19
- package/src/plugins/types/notification.ts +0 -51
- package/src/plugins/types/provider.ts +0 -237
- package/src/plugins/types/theme.ts +0 -78
- package/src/storage/database.ts +0 -623
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CredentialResult,
|
|
3
|
+
Credentials,
|
|
4
|
+
PluginContext,
|
|
5
|
+
ProviderAuth,
|
|
6
|
+
ProviderFetchContext,
|
|
7
|
+
ProviderPlugin,
|
|
8
|
+
ProviderUsageData,
|
|
9
|
+
} from "@tokentop/plugin-sdk";
|
|
10
|
+
|
|
11
|
+
const CHUTES_QUOTA_URL = "https://api.chutes.ai/users/me/quota_usage/me";
|
|
12
|
+
|
|
13
|
+
interface ChutesQuotaResponse {
|
|
14
|
+
quota?: number;
|
|
15
|
+
used?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const chutesPlugin: ProviderPlugin = {
|
|
19
|
+
apiVersion: 2,
|
|
20
|
+
id: "chutes",
|
|
21
|
+
type: "provider",
|
|
22
|
+
name: "Chutes",
|
|
23
|
+
version: "1.0.0",
|
|
24
|
+
|
|
25
|
+
meta: {
|
|
26
|
+
description: "Chutes AI API quota and usage tracking",
|
|
27
|
+
homepage: "https://chutes.ai",
|
|
28
|
+
brandColor: "#6366f1",
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
permissions: {
|
|
32
|
+
network: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
allowedDomains: ["api.chutes.ai"],
|
|
35
|
+
},
|
|
36
|
+
filesystem: {
|
|
37
|
+
read: true,
|
|
38
|
+
paths: ["~/.local/share/opencode"],
|
|
39
|
+
},
|
|
40
|
+
env: {
|
|
41
|
+
read: true,
|
|
42
|
+
vars: ["CHUTES_API_KEY"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
capabilities: {
|
|
47
|
+
usageLimits: true,
|
|
48
|
+
apiRateLimits: false,
|
|
49
|
+
tokenUsage: false,
|
|
50
|
+
actualCosts: false,
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
auth: {
|
|
54
|
+
async discover(ctx: PluginContext): Promise<CredentialResult> {
|
|
55
|
+
const entry = await ctx.authSources.opencode.getProviderEntry("chutes");
|
|
56
|
+
if (entry) {
|
|
57
|
+
if (entry.type === "api" && entry.key) {
|
|
58
|
+
return { ok: true, credentials: { apiKey: entry.key, source: "opencode" } };
|
|
59
|
+
}
|
|
60
|
+
if (entry.type === "wellknown" && (entry.token || entry.key)) {
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
credentials: { apiKey: (entry.token || entry.key)!, source: "opencode" },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (entry.type === "oauth" && entry.access) {
|
|
67
|
+
return { ok: true, credentials: { apiKey: entry.access, source: "opencode" } };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const apiKey = ctx.authSources.env.get("CHUTES_API_KEY");
|
|
72
|
+
if (apiKey) {
|
|
73
|
+
return { ok: true, credentials: { apiKey, source: "env" } };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
reason: "missing",
|
|
79
|
+
message: "No Chutes API key found. Configure Chutes in OpenCode.",
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
isConfigured(credentials: Credentials): boolean {
|
|
84
|
+
return !!(credentials.apiKey || credentials.oauth?.accessToken);
|
|
85
|
+
},
|
|
86
|
+
} satisfies ProviderAuth,
|
|
87
|
+
|
|
88
|
+
async fetchUsage(ctx: ProviderFetchContext): Promise<ProviderUsageData> {
|
|
89
|
+
const { credentials, http, logger: log } = ctx;
|
|
90
|
+
|
|
91
|
+
const token = credentials.apiKey || credentials.oauth?.accessToken;
|
|
92
|
+
|
|
93
|
+
if (!token) {
|
|
94
|
+
return {
|
|
95
|
+
fetchedAt: Date.now(),
|
|
96
|
+
error: "API key required. Configure Chutes in OpenCode.",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const response = await http.fetch(CHUTES_QUOTA_URL, {
|
|
102
|
+
method: "GET",
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${token}`,
|
|
105
|
+
Accept: "application/json",
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
log.warn("Failed to fetch Chutes usage", { status: response.status });
|
|
111
|
+
|
|
112
|
+
if (response.status === 429) {
|
|
113
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
114
|
+
const parsedRetryAfter = retryAfterHeader
|
|
115
|
+
? Number.parseInt(retryAfterHeader, 10)
|
|
116
|
+
: Number.NaN;
|
|
117
|
+
const retrySec =
|
|
118
|
+
Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
|
|
119
|
+
log.warn("Rate limited by Chutes API", { retryAfterHeader, retryAfterSec: retrySec });
|
|
120
|
+
return {
|
|
121
|
+
fetchedAt: Date.now(),
|
|
122
|
+
error: `Rate limited. Retry after ${retrySec}s.`,
|
|
123
|
+
rateLimited: true,
|
|
124
|
+
retryAfterMs: retrySec * 1000,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (response.status === 401 || response.status === 403) {
|
|
129
|
+
return {
|
|
130
|
+
fetchedAt: Date.now(),
|
|
131
|
+
error: "Authorization failed. Check your Chutes API key.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
fetchedAt: Date.now(),
|
|
137
|
+
error: `API error: ${response.status} ${response.statusText}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const data = (await response.json()) as ChutesQuotaResponse;
|
|
142
|
+
|
|
143
|
+
if (!data || typeof data.quota !== "number" || typeof data.used !== "number") {
|
|
144
|
+
return {
|
|
145
|
+
fetchedAt: Date.now(),
|
|
146
|
+
planType: "Chutes",
|
|
147
|
+
allowed: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const quota = data.quota;
|
|
152
|
+
const used = data.used;
|
|
153
|
+
const usedPercent = quota > 0 ? Math.round((used / quota) * 100) : 0;
|
|
154
|
+
|
|
155
|
+
const planType = getTierFromQuota(quota);
|
|
156
|
+
const resetAt = calculateNextReset();
|
|
157
|
+
const limitReached = usedPercent >= 100;
|
|
158
|
+
|
|
159
|
+
const result: ProviderUsageData = {
|
|
160
|
+
planType,
|
|
161
|
+
allowed: !limitReached,
|
|
162
|
+
limitReached,
|
|
163
|
+
fetchedAt: Date.now(),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (usedPercent !== null) {
|
|
167
|
+
result.limits = {
|
|
168
|
+
primary: {
|
|
169
|
+
usedPercent,
|
|
170
|
+
label: "Daily quota",
|
|
171
|
+
resetsAt: resetAt,
|
|
172
|
+
windowMinutes: 1440,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
log.error("Failed to fetch Chutes usage", { error: err });
|
|
180
|
+
return {
|
|
181
|
+
fetchedAt: Date.now(),
|
|
182
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
function getTierFromQuota(quota: number): string {
|
|
189
|
+
const tierMap: Record<number, string> = {
|
|
190
|
+
200: "Legacy",
|
|
191
|
+
300: "Base",
|
|
192
|
+
2000: "Plus",
|
|
193
|
+
5000: "Pro",
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return tierMap[quota] ?? `Custom (${quota})`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function calculateNextReset(): number {
|
|
200
|
+
const now = new Date();
|
|
201
|
+
const tomorrow = new Date(now);
|
|
202
|
+
tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
|
|
203
|
+
tomorrow.setUTCHours(0, 0, 0, 0);
|
|
204
|
+
return tomorrow.getTime();
|
|
205
|
+
}
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
ProviderFetchContext,
|
|
8
8
|
ProviderPlugin,
|
|
9
9
|
ProviderUsageData,
|
|
10
|
-
} from "
|
|
10
|
+
} from "@tokentop/plugin-sdk";
|
|
11
11
|
|
|
12
12
|
interface CodexCliCredentials {
|
|
13
13
|
access_token?: string;
|
|
@@ -178,6 +178,22 @@ export const codexPlugin: ProviderPlugin = {
|
|
|
178
178
|
const bodyText = await response.text().catch(() => "");
|
|
179
179
|
log.warn("Failed to fetch ChatGPT usage", { status: response.status, body: bodyText });
|
|
180
180
|
|
|
181
|
+
if (response.status === 429) {
|
|
182
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
183
|
+
const parsedRetryAfter = retryAfterHeader
|
|
184
|
+
? Number.parseInt(retryAfterHeader, 10)
|
|
185
|
+
: Number.NaN;
|
|
186
|
+
const retrySec =
|
|
187
|
+
Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
|
|
188
|
+
log.warn("Rate limited by ChatGPT API", { retryAfterHeader, retryAfterSec: retrySec });
|
|
189
|
+
return {
|
|
190
|
+
fetchedAt: Date.now(),
|
|
191
|
+
error: `Rate limited. Retry after ${retrySec}s.`,
|
|
192
|
+
rateLimited: true,
|
|
193
|
+
retryAfterMs: retrySec * 1000,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
181
197
|
if (response.status === 401) {
|
|
182
198
|
return {
|
|
183
199
|
fetchedAt: Date.now(),
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
ProviderUsageData,
|
|
10
10
|
RefreshedCredentials,
|
|
11
11
|
UsageLimit,
|
|
12
|
-
} from "
|
|
12
|
+
} from "@tokentop/plugin-sdk";
|
|
13
13
|
|
|
14
14
|
const GOOGLE_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
15
15
|
|
|
@@ -212,6 +212,22 @@ export const geminiPlugin: ProviderPlugin = {
|
|
|
212
212
|
});
|
|
213
213
|
|
|
214
214
|
if (!response.ok) {
|
|
215
|
+
if (response.status === 429) {
|
|
216
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
217
|
+
const parsedRetryAfter = retryAfterHeader
|
|
218
|
+
? Number.parseInt(retryAfterHeader, 10)
|
|
219
|
+
: Number.NaN;
|
|
220
|
+
const retrySec =
|
|
221
|
+
Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
|
|
222
|
+
log.warn("Rate limited by Google API", { retryAfterHeader, retryAfterSec: retrySec });
|
|
223
|
+
return {
|
|
224
|
+
fetchedAt: Date.now(),
|
|
225
|
+
error: `Rate limited. Retry after ${retrySec}s.`,
|
|
226
|
+
rateLimited: true,
|
|
227
|
+
retryAfterMs: retrySec * 1000,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
215
231
|
const errorText = await response.text().catch(() => "");
|
|
216
232
|
if (response.status === 401) {
|
|
217
233
|
return {
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
ProviderPlugin,
|
|
8
8
|
ProviderUsageData,
|
|
9
9
|
UsageLimit,
|
|
10
|
-
} from "
|
|
10
|
+
} from "@tokentop/plugin-sdk";
|
|
11
11
|
|
|
12
12
|
interface CopilotUserResponse {
|
|
13
13
|
access_type_sku?: string;
|
|
@@ -134,6 +134,22 @@ export const githubCopilotPlugin: ProviderPlugin = {
|
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
if (!response.ok) {
|
|
137
|
+
if (response.status === 429) {
|
|
138
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
139
|
+
const parsedRetryAfter = retryAfterHeader
|
|
140
|
+
? Number.parseInt(retryAfterHeader, 10)
|
|
141
|
+
: Number.NaN;
|
|
142
|
+
const retrySec =
|
|
143
|
+
Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
|
|
144
|
+
log.warn("Rate limited by GitHub API", { retryAfterHeader, retryAfterSec: retrySec });
|
|
145
|
+
return {
|
|
146
|
+
fetchedAt: Date.now(),
|
|
147
|
+
error: `Rate limited. Retry after ${retrySec}s.`,
|
|
148
|
+
rateLimited: true,
|
|
149
|
+
retryAfterMs: retrySec * 1000,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
137
153
|
if (response.status === 404) {
|
|
138
154
|
return {
|
|
139
155
|
planType: "None",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { anthropicPlugin } from "./anthropic.ts";
|
|
2
2
|
export { antigravityPlugin } from "./antigravity.ts";
|
|
3
|
+
export { chutesPlugin } from "./chutes.ts";
|
|
3
4
|
export { codexPlugin } from "./codex.ts";
|
|
4
5
|
export { geminiPlugin } from "./gemini.ts";
|
|
5
6
|
export { githubCopilotPlugin } from "./github-copilot.ts";
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
ProviderFetchContext,
|
|
7
7
|
ProviderPlugin,
|
|
8
8
|
ProviderUsageData,
|
|
9
|
-
} from "
|
|
9
|
+
} from "@tokentop/plugin-sdk";
|
|
10
10
|
|
|
11
11
|
const MINIMAX_USAGE_URL = "https://platform.minimax.io/v1/api/openplatform/coding_plan/remains";
|
|
12
12
|
|
|
@@ -151,6 +151,22 @@ function createMinimaxPlugin(id: string, name: string): ProviderPlugin {
|
|
|
151
151
|
if (!response.ok) {
|
|
152
152
|
log.warn("Failed to fetch MiniMax usage", { status: response.status });
|
|
153
153
|
|
|
154
|
+
if (response.status === 429) {
|
|
155
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
156
|
+
const parsedRetryAfter = retryAfterHeader
|
|
157
|
+
? Number.parseInt(retryAfterHeader, 10)
|
|
158
|
+
: Number.NaN;
|
|
159
|
+
const retrySec =
|
|
160
|
+
Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
|
|
161
|
+
log.warn("Rate limited by MiniMax API", { retryAfterHeader, retryAfterSec: retrySec });
|
|
162
|
+
return {
|
|
163
|
+
fetchedAt: Date.now(),
|
|
164
|
+
error: `Rate limited. Retry after ${retrySec}s.`,
|
|
165
|
+
rateLimited: true,
|
|
166
|
+
retryAfterMs: retrySec * 1000,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
154
170
|
if (response.status === 401 || response.status === 403) {
|
|
155
171
|
return {
|
|
156
172
|
fetchedAt: Date.now(),
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
ProviderFetchContext,
|
|
7
7
|
ProviderPlugin,
|
|
8
8
|
ProviderUsageData,
|
|
9
|
-
} from "
|
|
9
|
+
} from "@tokentop/plugin-sdk";
|
|
10
10
|
|
|
11
11
|
interface UsageBucket {
|
|
12
12
|
object: string;
|
|
@@ -49,6 +49,15 @@ interface CostsResponse {
|
|
|
49
49
|
has_more: boolean;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
class RateLimitError extends Error {
|
|
53
|
+
readonly retryAfterMs: number;
|
|
54
|
+
constructor(retryAfterMs: number) {
|
|
55
|
+
super(`Rate limited. Retry after ${retryAfterMs / 1000}s.`);
|
|
56
|
+
this.name = "RateLimitError";
|
|
57
|
+
this.retryAfterMs = retryAfterMs;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
export const openaiApiPlugin: ProviderPlugin = {
|
|
53
62
|
apiVersion: 2,
|
|
54
63
|
id: "openai-api",
|
|
@@ -145,6 +154,21 @@ export const openaiApiPlugin: ProviderPlugin = {
|
|
|
145
154
|
fetchCosts(http, adminKey, oneDayAgo),
|
|
146
155
|
]);
|
|
147
156
|
|
|
157
|
+
// If either helper hit a 429, surface rate-limit info immediately
|
|
158
|
+
for (const settled of [usageResult, costsResult]) {
|
|
159
|
+
if (settled.status === "rejected" && settled.reason instanceof RateLimitError) {
|
|
160
|
+
log.warn("Rate limited by OpenAI API", { retryAfterMs: settled.reason.retryAfterMs });
|
|
161
|
+
return {
|
|
162
|
+
planType: "API",
|
|
163
|
+
allowed: true,
|
|
164
|
+
fetchedAt: Date.now(),
|
|
165
|
+
error: settled.reason.message,
|
|
166
|
+
rateLimited: true,
|
|
167
|
+
retryAfterMs: settled.reason.retryAfterMs,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
148
172
|
const result: ProviderUsageData = {
|
|
149
173
|
planType: "API",
|
|
150
174
|
allowed: true,
|
|
@@ -208,6 +232,16 @@ export const openaiApiPlugin: ProviderPlugin = {
|
|
|
208
232
|
|
|
209
233
|
return result;
|
|
210
234
|
} catch (err) {
|
|
235
|
+
if (err instanceof RateLimitError) {
|
|
236
|
+
return {
|
|
237
|
+
planType: "API",
|
|
238
|
+
allowed: true,
|
|
239
|
+
fetchedAt: Date.now(),
|
|
240
|
+
error: err.message,
|
|
241
|
+
rateLimited: true,
|
|
242
|
+
retryAfterMs: err.retryAfterMs,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
211
245
|
const msg = err instanceof Error ? err.message : String(err);
|
|
212
246
|
log.error("OpenAI fetch error", { error: msg });
|
|
213
247
|
|
|
@@ -236,6 +270,15 @@ async function fetchCompletionsUsage(
|
|
|
236
270
|
});
|
|
237
271
|
|
|
238
272
|
if (!response.ok) {
|
|
273
|
+
if (response.status === 429) {
|
|
274
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
275
|
+
const parsedRetryAfter = retryAfterHeader
|
|
276
|
+
? Number.parseInt(retryAfterHeader, 10)
|
|
277
|
+
: Number.NaN;
|
|
278
|
+
const retrySec =
|
|
279
|
+
Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
|
|
280
|
+
throw new RateLimitError(retrySec * 1000);
|
|
281
|
+
}
|
|
239
282
|
if (response.status === 403) {
|
|
240
283
|
return null;
|
|
241
284
|
}
|
|
@@ -260,6 +303,15 @@ async function fetchCosts(
|
|
|
260
303
|
});
|
|
261
304
|
|
|
262
305
|
if (!response.ok) {
|
|
306
|
+
if (response.status === 429) {
|
|
307
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
308
|
+
const parsedRetryAfter = retryAfterHeader
|
|
309
|
+
? Number.parseInt(retryAfterHeader, 10)
|
|
310
|
+
: Number.NaN;
|
|
311
|
+
const retrySec =
|
|
312
|
+
Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
|
|
313
|
+
throw new RateLimitError(retrySec * 1000);
|
|
314
|
+
}
|
|
263
315
|
if (response.status === 403) {
|
|
264
316
|
return null;
|
|
265
317
|
}
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
ProviderFetchContext,
|
|
7
7
|
ProviderPlugin,
|
|
8
8
|
ProviderUsageData,
|
|
9
|
-
} from "
|
|
9
|
+
} from "@tokentop/plugin-sdk";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Perplexity Shadow API Discovery (January 2026):
|
|
@@ -135,11 +135,20 @@ export const perplexityPlugin: ProviderPlugin = {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
if (response.status === 429) {
|
|
138
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
139
|
+
const parsedRetryAfter = retryAfterHeader
|
|
140
|
+
? Number.parseInt(retryAfterHeader, 10)
|
|
141
|
+
: Number.NaN;
|
|
142
|
+
const retrySec =
|
|
143
|
+
Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
|
|
144
|
+
log.warn("Rate limited by Perplexity API", { retryAfterHeader, retryAfterSec: retrySec });
|
|
138
145
|
return {
|
|
139
146
|
planType: "API",
|
|
140
147
|
allowed: true,
|
|
141
148
|
fetchedAt: Date.now(),
|
|
142
|
-
error:
|
|
149
|
+
error: `Rate limited. Retry after ${retrySec}s.`,
|
|
150
|
+
rateLimited: true,
|
|
151
|
+
retryAfterMs: retrySec * 1000,
|
|
143
152
|
};
|
|
144
153
|
}
|
|
145
154
|
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
ProviderPlugin,
|
|
8
8
|
ProviderUsageData,
|
|
9
9
|
UsageLimit,
|
|
10
|
-
} from "
|
|
10
|
+
} from "@tokentop/plugin-sdk";
|
|
11
11
|
|
|
12
12
|
const ZAI_USAGE_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
|
|
13
13
|
|
|
@@ -124,6 +124,22 @@ export const zaiCodingPlanPlugin: ProviderPlugin = {
|
|
|
124
124
|
if (!response.ok) {
|
|
125
125
|
log.warn("Failed to fetch Z.ai usage", { status: response.status });
|
|
126
126
|
|
|
127
|
+
if (response.status === 429) {
|
|
128
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
129
|
+
const parsedRetryAfter = retryAfterHeader
|
|
130
|
+
? Number.parseInt(retryAfterHeader, 10)
|
|
131
|
+
: Number.NaN;
|
|
132
|
+
const retrySec =
|
|
133
|
+
Number.isFinite(parsedRetryAfter) && parsedRetryAfter > 0 ? parsedRetryAfter : 300;
|
|
134
|
+
log.warn("Rate limited by Z.ai API", { retryAfterHeader, retryAfterSec: retrySec });
|
|
135
|
+
return {
|
|
136
|
+
fetchedAt: Date.now(),
|
|
137
|
+
error: `Rate limited. Retry after ${retrySec}s.`,
|
|
138
|
+
rateLimited: true,
|
|
139
|
+
retryAfterMs: retrySec * 1000,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
if (response.status === 401 || response.status === 403) {
|
|
128
144
|
return {
|
|
129
145
|
fetchedAt: Date.now(),
|
package/src/plugins/registry.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
import type { PluginsConfig } from "@/config/schema.ts";
|
|
2
|
-
import {
|
|
3
|
-
discoverLocalPlugins,
|
|
4
|
-
loadLocalPlugin,
|
|
5
|
-
loadNpmPlugin,
|
|
6
|
-
resolvePluginPath,
|
|
7
|
-
} from "./loader.ts";
|
|
8
|
-
import { installAllNpmPlugins, resolveNpmPluginPath } from "./npm-installer.ts";
|
|
9
1
|
import type {
|
|
10
2
|
AgentPlugin,
|
|
11
3
|
AnyPlugin,
|
|
@@ -14,7 +6,15 @@ import type {
|
|
|
14
6
|
PluginType,
|
|
15
7
|
ProviderPlugin,
|
|
16
8
|
ThemePlugin,
|
|
17
|
-
} from "
|
|
9
|
+
} from "@tokentop/plugin-sdk";
|
|
10
|
+
import type { PluginsConfig } from "@/config/schema.ts";
|
|
11
|
+
import {
|
|
12
|
+
discoverLocalPlugins,
|
|
13
|
+
loadLocalPlugin,
|
|
14
|
+
loadNpmPlugin,
|
|
15
|
+
resolvePluginPath,
|
|
16
|
+
} from "./loader.ts";
|
|
17
|
+
import { installAllNpmPlugins, resolveNpmPluginPath } from "./npm-installer.ts";
|
|
18
18
|
|
|
19
19
|
export type PluginSource = "builtin" | "local" | "npm";
|
|
20
20
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { PluginPermissionError } from "@tokentop/plugin-sdk";
|
|
2
3
|
import {
|
|
3
4
|
deepFreeze,
|
|
4
5
|
getActivePluginGuard,
|
|
5
6
|
installGlobalFetchGuard,
|
|
6
7
|
runInPluginGuard,
|
|
7
8
|
} from "./sandbox-guard.ts";
|
|
8
|
-
import { PluginPermissionError } from "./types/base.ts";
|
|
9
9
|
|
|
10
10
|
const originalFetch = globalThis.fetch;
|
|
11
11
|
const fetchCalls: string[] = [];
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* 2. **Deep-freeze** — prevents plugins from mutating the PluginContext.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { PluginPermissionError, type PluginPermissions } from "@tokentop/plugin-sdk";
|
|
14
15
|
import { AsyncLocalStorage } from "async_hooks";
|
|
15
16
|
import { createPluginLogger } from "./sandbox.ts";
|
|
16
|
-
import { PluginPermissionError, type PluginPermissions } from "./types/base.ts";
|
|
17
17
|
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
// Plugin execution context (AsyncLocalStorage)
|
package/src/plugins/sandbox.ts
CHANGED