@tokentop/ttop 0.3.0 → 0.5.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/package.json +9 -4
- package/src/agents/aggregator.ts +8 -1
- package/src/agents/types.ts +3 -0
- package/src/cli.ts +2 -2
- package/src/demo/simulator.ts +1 -1
- package/src/plugins/agents/index.ts +2 -0
- package/src/plugins/auth-sources.ts +2 -3
- package/src/plugins/lifecycle.ts +17 -13
- 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 +9 -2
- package/src/plugins/notifications/visual-flash.ts +13 -3
- package/src/plugins/plugin-context-factory.ts +4 -19
- package/src/plugins/providers/anthropic.ts +2 -2
- package/src/plugins/providers/antigravity.ts +1 -1
- package/src/plugins/providers/chutes.ts +189 -0
- package/src/plugins/providers/codex.ts +1 -1
- package/src/plugins/providers/gemini.ts +1 -1
- package/src/plugins/providers/github-copilot.ts +1 -1
- package/src/plugins/providers/index.ts +1 -0
- package/src/plugins/providers/minimax.ts +1 -1
- package/src/plugins/providers/openai-api.ts +1 -1
- package/src/plugins/providers/opencode-zen.ts +1 -1
- package/src/plugins/providers/perplexity.ts +1 -1
- package/src/plugins/providers/zai.ts +4 -2
- package/src/plugins/registry.ts +51 -46
- 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 +1 -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/persistence-service.ts +206 -0
- package/src/storage/repos/pluginStorage.ts +35 -0
- package/src/storage/retention.ts +146 -0
- package/src/tui/App.tsx +1 -1
- package/src/tui/components/DebugPanel.tsx +23 -10
- package/src/tui/components/ProviderCard.tsx +1 -1
- package/src/tui/components/ProviderDetailPanel.tsx +1 -1
- package/src/tui/components/SessionDetailsDrawer.tsx +10 -0
- package/src/tui/components/SessionsTable.tsx +15 -4
- package/src/tui/components/SettingsModal.tsx +383 -76
- package/src/tui/components/ThemePicker.tsx +1 -1
- package/src/tui/contexts/AgentSessionContext.tsx +44 -80
- package/src/tui/contexts/PluginContext.tsx +13 -3
- package/src/tui/contexts/RealTimeActivityContext.tsx +33 -20
- package/src/tui/contexts/StorageContext.tsx +42 -130
- package/src/tui/contexts/ThemeContext.tsx +3 -3
- package/src/tui/hooks/useExitAnimation.ts +4 -0
- package/src/tui/hooks/useSessionEnrichedProviders.ts +1 -1
- package/src/tui/views/Dashboard.tsx +40 -3
- package/src/tui/views/ProjectsView.tsx +29 -6
- package/src/tui/views/RealTimeDashboard.tsx +29 -8
- package/src/tui/views/SettingsView.tsx +9 -4
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokentop/ttop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Real-time AI token usage monitor - htop for your API costs",
|
|
5
5
|
"author": "Nigel Bazzeghin <nbazzeghin@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,11 +33,13 @@
|
|
|
33
33
|
"snapshot": "bun src/tui/debug/snapshot.tsx"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@opentui/core": "0.1.
|
|
37
|
-
"@opentui/react": "0.1.
|
|
36
|
+
"@opentui/core": "0.1.84",
|
|
37
|
+
"@opentui/react": "0.1.84",
|
|
38
38
|
"@tokentop/agent-claude-code": "^1.1.0",
|
|
39
|
+
"@tokentop/agent-cursor": "^1.2.0",
|
|
39
40
|
"@tokentop/agent-opencode": "^1.0.0",
|
|
40
|
-
"@tokentop/
|
|
41
|
+
"@tokentop/agent-copilot-cli": "^1.0.0",
|
|
42
|
+
"@tokentop/plugin-sdk": "^1.3.0",
|
|
41
43
|
"react": "^19.0.0",
|
|
42
44
|
"react-devtools-core": "^7.0.1",
|
|
43
45
|
"zod": "^4.0.0"
|
|
@@ -67,5 +69,8 @@
|
|
|
67
69
|
},
|
|
68
70
|
"engines": {
|
|
69
71
|
"bun": ">=1.0.0"
|
|
72
|
+
},
|
|
73
|
+
"overrides": {
|
|
74
|
+
"@types/node": "^25.0.0"
|
|
70
75
|
}
|
|
71
76
|
}
|
package/src/agents/aggregator.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SessionUsageData } from "
|
|
1
|
+
import type { SessionUsageData } from "@tokentop/plugin-sdk";
|
|
2
2
|
import {
|
|
3
3
|
type AgentId,
|
|
4
4
|
type AgentName,
|
|
@@ -189,6 +189,13 @@ export function aggregateSessionUsage(options: AggregateOptions): AgentSessionAg
|
|
|
189
189
|
};
|
|
190
190
|
if (session.sessionName) aggregate.sessionName = session.sessionName;
|
|
191
191
|
if (session.projectPath) aggregate.projectPath = session.projectPath;
|
|
192
|
+
|
|
193
|
+
// Propagate metadata — session is estimated if any row is estimated
|
|
194
|
+
const sessionRows = rows.filter((r) => r.sessionId === sessionId);
|
|
195
|
+
const anyEstimated = sessionRows.some((r) => (r.metadata?.isEstimated as boolean) === true);
|
|
196
|
+
if (anyEstimated) {
|
|
197
|
+
aggregate.metadata = { isEstimated: true };
|
|
198
|
+
}
|
|
192
199
|
results.push(aggregate);
|
|
193
200
|
}
|
|
194
201
|
|
package/src/agents/types.ts
CHANGED
|
@@ -60,6 +60,9 @@ export interface AgentSessionAggregate {
|
|
|
60
60
|
|
|
61
61
|
/** @internal Keyed by `providerId::modelId`. Set by aggregator, consumed by costing. */
|
|
62
62
|
_streamWindowedTokens?: Map<string, StreamWindowedTokens>;
|
|
63
|
+
|
|
64
|
+
/** Plugin-provided metadata merged from underlying SessionUsageData rows. */
|
|
65
|
+
metadata?: Record<string, unknown>;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
export interface AgentInfo {
|
package/src/cli.ts
CHANGED
|
@@ -114,7 +114,7 @@ async function main() {
|
|
|
114
114
|
}
|
|
115
115
|
const seedStr = commandArgs[++i];
|
|
116
116
|
const seed = seedStr ? parseInt(seedStr, 10) : NaN;
|
|
117
|
-
if (isNaN(seed)) {
|
|
117
|
+
if (Number.isNaN(seed)) {
|
|
118
118
|
console.error("Error: --seed requires a number");
|
|
119
119
|
process.exit(1);
|
|
120
120
|
}
|
|
@@ -137,7 +137,7 @@ async function main() {
|
|
|
137
137
|
if (arg === "-r" || arg === "--refresh") {
|
|
138
138
|
const intervalStr = commandArgs[++i];
|
|
139
139
|
const interval = intervalStr ? parseInt(intervalStr, 10) : NaN;
|
|
140
|
-
if (isNaN(interval) || interval < 1000) {
|
|
140
|
+
if (Number.isNaN(interval) || interval < 1000) {
|
|
141
141
|
console.error("Error: --refresh must be a number >= 1000");
|
|
142
142
|
process.exit(1);
|
|
143
143
|
}
|
package/src/demo/simulator.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { ProviderUsageData } from "@tokentop/plugin-sdk";
|
|
1
2
|
import type { AgentSessionAggregate, AgentSessionStream } from "@/agents/types.ts";
|
|
2
|
-
import type { ProviderUsageData } from "@/plugins/types/provider.ts";
|
|
3
3
|
import type { UsageEventInsert } from "@/storage/types.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { default as claudeCodeAgentPlugin } from "@tokentop/agent-claude-code";
|
|
2
|
+
export { default as copilotCliAgentPlugin } from "@tokentop/agent-copilot-cli";
|
|
3
|
+
export { default as cursorAgentPlugin } from "@tokentop/agent-cursor";
|
|
2
4
|
export { default as opencodeAgentPlugin } from "@tokentop/agent-opencode";
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import type { AuthSources, OpenCodeAuthEntry, PluginPermissions } from "@tokentop/plugin-sdk";
|
|
2
|
+
import { PluginPermissionError } from "@tokentop/plugin-sdk";
|
|
1
3
|
import * as fs from "fs/promises";
|
|
2
4
|
import * as os from "os";
|
|
3
5
|
import * as path from "path";
|
|
4
|
-
import type { PluginPermissions } from "./types/base.ts";
|
|
5
|
-
import { PluginPermissionError } from "./types/base.ts";
|
|
6
|
-
import type { AuthSources, OpenCodeAuthEntry } from "./types/provider.ts";
|
|
7
6
|
|
|
8
7
|
const OPENCODE_AUTH_PATH = path.join(os.homedir(), ".local/share/opencode/auth.json");
|
|
9
8
|
const OPENCODE_CONFIG_PATH = path.join(os.homedir(), ".config/opencode/opencode.json");
|
package/src/plugins/lifecycle.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* with safeInvoke for error isolation and circuit breaker protection.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { BasePlugin, PluginLifecycleContext, PluginLogger } from "@tokentop/plugin-sdk";
|
|
8
9
|
import { safeInvoke } from "./plugin-host.ts";
|
|
9
10
|
import { pluginRegistry } from "./registry.ts";
|
|
10
11
|
import { createPluginLogger } from "./sandbox.ts";
|
|
11
12
|
import { runInPluginGuard } from "./sandbox-guard.ts";
|
|
12
|
-
import type { BasePlugin, PluginLifecycleContext, PluginLogger } from "./types/base.ts";
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// State machine
|
|
@@ -91,25 +91,29 @@ class PluginLifecycleManager {
|
|
|
91
91
|
async initializeAll(): Promise<void> {
|
|
92
92
|
const plugins = pluginRegistry.getAllPlugins();
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
await Promise.all(
|
|
95
|
+
plugins.map(async (plugin) => {
|
|
96
|
+
const entry = this.getOrCreate(plugin);
|
|
97
|
+
if (entry.state !== "loaded") return;
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
const ok = await this.invokeHook(plugin, "initialize");
|
|
100
|
+
entry.state = ok ? "initialized" : "failed";
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
async startAll(): Promise<void> {
|
|
104
106
|
const plugins = pluginRegistry.getAllPlugins();
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
await Promise.all(
|
|
109
|
+
plugins.map(async (plugin) => {
|
|
110
|
+
const entry = this.getOrCreate(plugin);
|
|
111
|
+
if (entry.state !== "initialized") return;
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
const ok = await this.invokeHook(plugin, "start");
|
|
114
|
+
entry.state = ok ? "started" : "failed";
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
async stopAll(): Promise<void> {
|
package/src/plugins/loader.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
import type { AnyPlugin, BasePlugin, PluginType } from "@tokentop/plugin-sdk";
|
|
2
|
+
import { CURRENT_API_VERSION } from "@tokentop/plugin-sdk";
|
|
1
3
|
import * as fs from "fs/promises";
|
|
2
4
|
import * as os from "os";
|
|
3
5
|
import * as path from "path";
|
|
4
6
|
import { PATHS } from "@/storage/paths.ts";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
|
|
8
|
+
interface PluginValidationResult {
|
|
9
|
+
valid: boolean;
|
|
10
|
+
errors: string[];
|
|
11
|
+
warnings: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PluginLoadResult<T extends BasePlugin = BasePlugin> {
|
|
15
|
+
success: boolean;
|
|
16
|
+
plugin?: T;
|
|
17
|
+
error?: string;
|
|
18
|
+
source: "builtin" | "local" | "npm";
|
|
19
|
+
}
|
|
12
20
|
|
|
13
21
|
const CUSTOM_PLUGINS_DIR = PATHS.config.plugins;
|
|
14
22
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NotificationEvent,
|
|
3
|
+
NotificationPlugin,
|
|
4
|
+
ProviderUsageData,
|
|
5
|
+
} from "@tokentop/plugin-sdk";
|
|
1
6
|
import type { AppConfig } from "@/config/schema.ts";
|
|
2
7
|
import { safeInvoke } from "./plugin-host.ts";
|
|
3
8
|
import { createPluginLogger } from "./sandbox.ts";
|
|
4
|
-
import type { NotificationEvent, NotificationPlugin } from "./types/notification.ts";
|
|
5
|
-
import type { ProviderUsageData } from "./types/provider.ts";
|
|
6
9
|
|
|
7
10
|
interface DedupEntry {
|
|
8
11
|
key: string;
|
|
@@ -2,7 +2,7 @@ import type {
|
|
|
2
2
|
NotificationContext,
|
|
3
3
|
NotificationEvent,
|
|
4
4
|
NotificationPlugin,
|
|
5
|
-
} from "
|
|
5
|
+
} from "@tokentop/plugin-sdk";
|
|
6
6
|
|
|
7
7
|
export const terminalBellPlugin: NotificationPlugin = {
|
|
8
8
|
apiVersion: 2,
|
|
@@ -18,8 +18,15 @@ export const terminalBellPlugin: NotificationPlugin = {
|
|
|
18
18
|
permissions: {},
|
|
19
19
|
|
|
20
20
|
configSchema: {
|
|
21
|
-
enabled: {
|
|
21
|
+
enabled: {
|
|
22
|
+
type: "boolean",
|
|
23
|
+
label: "Enabled",
|
|
24
|
+
default: true,
|
|
25
|
+
description: "Enable terminal bell notifications",
|
|
26
|
+
},
|
|
22
27
|
minSeverity: {
|
|
28
|
+
label: "Minimum severity",
|
|
29
|
+
description: "Only trigger bell for alerts at this severity or higher.",
|
|
23
30
|
type: "select",
|
|
24
31
|
default: "warning",
|
|
25
32
|
options: [
|
|
@@ -2,7 +2,7 @@ import type {
|
|
|
2
2
|
NotificationContext,
|
|
3
3
|
NotificationEvent,
|
|
4
4
|
NotificationPlugin,
|
|
5
|
-
} from "
|
|
5
|
+
} from "@tokentop/plugin-sdk";
|
|
6
6
|
|
|
7
7
|
export const visualFlashPlugin: NotificationPlugin = {
|
|
8
8
|
apiVersion: 2,
|
|
@@ -18,8 +18,18 @@ export const visualFlashPlugin: NotificationPlugin = {
|
|
|
18
18
|
permissions: {},
|
|
19
19
|
|
|
20
20
|
configSchema: {
|
|
21
|
-
enabled: {
|
|
22
|
-
|
|
21
|
+
enabled: {
|
|
22
|
+
type: "boolean",
|
|
23
|
+
label: "Enabled",
|
|
24
|
+
default: true,
|
|
25
|
+
description: "Enable visual flash notifications",
|
|
26
|
+
},
|
|
27
|
+
duration: {
|
|
28
|
+
type: "number",
|
|
29
|
+
label: "Flash duration (ms)",
|
|
30
|
+
default: 100,
|
|
31
|
+
description: "Flash duration in milliseconds",
|
|
32
|
+
},
|
|
23
33
|
},
|
|
24
34
|
|
|
25
35
|
supports(event: NotificationEvent): boolean {
|
|
@@ -1,27 +1,17 @@
|
|
|
1
|
+
import type { PluginContext, PluginPermissions, PluginStorage } from "@tokentop/plugin-sdk";
|
|
2
|
+
import { isDatabaseInitialized } from "@/storage/db.ts";
|
|
1
3
|
import {
|
|
2
|
-
getDatabase,
|
|
3
4
|
pluginStorageDelete,
|
|
4
5
|
pluginStorageGet,
|
|
5
6
|
pluginStorageHas,
|
|
6
7
|
pluginStorageSet,
|
|
7
|
-
} from "@/storage/
|
|
8
|
+
} from "@/storage/repos/pluginStorage.ts";
|
|
8
9
|
import { createAuthSources } from "./auth-sources.ts";
|
|
9
10
|
import { createPluginLogger, createSandboxedHttpClient } from "./sandbox.ts";
|
|
10
11
|
import { deepFreeze } from "./sandbox-guard.ts";
|
|
11
|
-
import type { PluginPermissions } from "./types/base.ts";
|
|
12
|
-
import type { PluginContext, PluginStorage } from "./types/provider.ts";
|
|
13
|
-
|
|
14
|
-
let dbAvailable: boolean | null = null;
|
|
15
12
|
|
|
16
13
|
function isDbAvailable(): boolean {
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
getDatabase();
|
|
20
|
-
dbAvailable = true;
|
|
21
|
-
} catch {
|
|
22
|
-
dbAvailable = false;
|
|
23
|
-
}
|
|
24
|
-
return dbAvailable;
|
|
14
|
+
return isDatabaseInitialized();
|
|
25
15
|
}
|
|
26
16
|
|
|
27
17
|
function createPluginStorage(pluginId: string): PluginStorage {
|
|
@@ -44,11 +34,6 @@ function createPluginStorage(pluginId: string): PluginStorage {
|
|
|
44
34
|
},
|
|
45
35
|
};
|
|
46
36
|
}
|
|
47
|
-
|
|
48
|
-
export function resetDbAvailableCache(): void {
|
|
49
|
-
dbAvailable = null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
37
|
export function createPluginContext(
|
|
53
38
|
pluginId: string,
|
|
54
39
|
permissions: PluginPermissions,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { execFileSync } from "child_process";
|
|
2
1
|
import type {
|
|
3
2
|
CredentialResult,
|
|
4
3
|
Credentials,
|
|
@@ -8,7 +7,8 @@ import type {
|
|
|
8
7
|
ProviderFetchContext,
|
|
9
8
|
ProviderPlugin,
|
|
10
9
|
ProviderUsageData,
|
|
11
|
-
} from "
|
|
10
|
+
} from "@tokentop/plugin-sdk";
|
|
11
|
+
import { execFileSync } from "child_process";
|
|
12
12
|
|
|
13
13
|
const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
14
14
|
|
|
@@ -0,0 +1,189 @@
|
|
|
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 === 401 || response.status === 403) {
|
|
113
|
+
return {
|
|
114
|
+
fetchedAt: Date.now(),
|
|
115
|
+
error: "Authorization failed. Check your Chutes API key.",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
fetchedAt: Date.now(),
|
|
121
|
+
error: `API error: ${response.status} ${response.statusText}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const data = (await response.json()) as ChutesQuotaResponse;
|
|
126
|
+
|
|
127
|
+
if (!data || typeof data.quota !== "number" || typeof data.used !== "number") {
|
|
128
|
+
return {
|
|
129
|
+
fetchedAt: Date.now(),
|
|
130
|
+
planType: "Chutes",
|
|
131
|
+
allowed: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const quota = data.quota;
|
|
136
|
+
const used = data.used;
|
|
137
|
+
const usedPercent = quota > 0 ? Math.round((used / quota) * 100) : 0;
|
|
138
|
+
|
|
139
|
+
const planType = getTierFromQuota(quota);
|
|
140
|
+
const resetAt = calculateNextReset();
|
|
141
|
+
const limitReached = usedPercent >= 100;
|
|
142
|
+
|
|
143
|
+
const result: ProviderUsageData = {
|
|
144
|
+
planType,
|
|
145
|
+
allowed: !limitReached,
|
|
146
|
+
limitReached,
|
|
147
|
+
fetchedAt: Date.now(),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (usedPercent !== null) {
|
|
151
|
+
result.limits = {
|
|
152
|
+
primary: {
|
|
153
|
+
usedPercent,
|
|
154
|
+
label: "Daily quota",
|
|
155
|
+
resetsAt: resetAt,
|
|
156
|
+
windowMinutes: 1440,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
log.error("Failed to fetch Chutes usage", { error: err });
|
|
164
|
+
return {
|
|
165
|
+
fetchedAt: Date.now(),
|
|
166
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
function getTierFromQuota(quota: number): string {
|
|
173
|
+
const tierMap: Record<number, string> = {
|
|
174
|
+
200: "Legacy",
|
|
175
|
+
300: "Base",
|
|
176
|
+
2000: "Plus",
|
|
177
|
+
5000: "Pro",
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return tierMap[quota] ?? `Custom (${quota})`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function calculateNextReset(): number {
|
|
184
|
+
const now = new Date();
|
|
185
|
+
const tomorrow = new Date(now);
|
|
186
|
+
tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
|
|
187
|
+
tomorrow.setUTCHours(0, 0, 0, 0);
|
|
188
|
+
return tomorrow.getTime();
|
|
189
|
+
}
|
|
@@ -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";
|
|
@@ -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
|
|
|
@@ -64,7 +64,9 @@ export const zaiCodingPlanPlugin: ProviderPlugin = {
|
|
|
64
64
|
auth: {
|
|
65
65
|
async discover(ctx: PluginContext): Promise<CredentialResult> {
|
|
66
66
|
// 1. Try OpenCode auth (api, wellknown, or oauth)
|
|
67
|
-
const entry =
|
|
67
|
+
const entry =
|
|
68
|
+
(await ctx.authSources.opencode.getProviderEntry("zhipuai-coding-plan")) ||
|
|
69
|
+
(await ctx.authSources.opencode.getProviderEntry("zai-coding-plan"));
|
|
68
70
|
if (entry) {
|
|
69
71
|
if (entry.type === "api" && entry.key) {
|
|
70
72
|
return { ok: true, credentials: { apiKey: entry.key, source: "opencode" } };
|