@tokentop/ttop 0.2.2

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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -0
  3. package/bin/ttop.js +49 -0
  4. package/package.json +68 -0
  5. package/src/agents/aggregator.ts +176 -0
  6. package/src/agents/costing.ts +74 -0
  7. package/src/agents/index.ts +12 -0
  8. package/src/agents/types.ts +71 -0
  9. package/src/cli.ts +167 -0
  10. package/src/config/schema.test.ts +166 -0
  11. package/src/config/schema.ts +165 -0
  12. package/src/demo/simulator.ts +745 -0
  13. package/src/plugins/agents/index.ts +2 -0
  14. package/src/plugins/auth-sources.ts +186 -0
  15. package/src/plugins/lifecycle.ts +177 -0
  16. package/src/plugins/loader.test.ts +195 -0
  17. package/src/plugins/loader.ts +282 -0
  18. package/src/plugins/notification-bus.ts +153 -0
  19. package/src/plugins/notifications/index.ts +2 -0
  20. package/src/plugins/notifications/terminal-bell.ts +66 -0
  21. package/src/plugins/notifications/visual-flash.ts +57 -0
  22. package/src/plugins/npm-installer.ts +164 -0
  23. package/src/plugins/plugin-context-factory.ts +67 -0
  24. package/src/plugins/plugin-host.ts +205 -0
  25. package/src/plugins/providers/anthropic.ts +325 -0
  26. package/src/plugins/providers/antigravity.ts +309 -0
  27. package/src/plugins/providers/codex.ts +250 -0
  28. package/src/plugins/providers/gemini.ts +391 -0
  29. package/src/plugins/providers/github-copilot.ts +194 -0
  30. package/src/plugins/providers/index.ts +12 -0
  31. package/src/plugins/providers/minimax.ts +223 -0
  32. package/src/plugins/providers/openai-api.ts +250 -0
  33. package/src/plugins/providers/opencode-zen.ts +125 -0
  34. package/src/plugins/providers/perplexity.ts +177 -0
  35. package/src/plugins/providers/zai.ts +225 -0
  36. package/src/plugins/registry.ts +251 -0
  37. package/src/plugins/sandbox-guard.test.ts +191 -0
  38. package/src/plugins/sandbox-guard.ts +155 -0
  39. package/src/plugins/sandbox.ts +138 -0
  40. package/src/plugins/themes/catppuccin-latte.ts +45 -0
  41. package/src/plugins/themes/catppuccin-mocha.ts +45 -0
  42. package/src/plugins/themes/claude-code.ts +45 -0
  43. package/src/plugins/themes/dracula.ts +45 -0
  44. package/src/plugins/themes/github-light.ts +45 -0
  45. package/src/plugins/themes/gruvbox-dark.ts +45 -0
  46. package/src/plugins/themes/gruvbox-light.ts +45 -0
  47. package/src/plugins/themes/index.ts +15 -0
  48. package/src/plugins/themes/kanagawa.ts +45 -0
  49. package/src/plugins/themes/nord.ts +45 -0
  50. package/src/plugins/themes/one-dark.ts +45 -0
  51. package/src/plugins/themes/opencode.ts +47 -0
  52. package/src/plugins/themes/rose-pine-dawn.ts +45 -0
  53. package/src/plugins/themes/rose-pine.ts +45 -0
  54. package/src/plugins/themes/solarized-light.ts +45 -0
  55. package/src/plugins/themes/tokyo-night.ts +49 -0
  56. package/src/plugins/types/agent.ts +108 -0
  57. package/src/plugins/types/base.ts +267 -0
  58. package/src/plugins/types/index.ts +19 -0
  59. package/src/plugins/types/notification.ts +51 -0
  60. package/src/plugins/types/provider.ts +233 -0
  61. package/src/plugins/types/theme.ts +70 -0
  62. package/src/plugins/update-checker.ts +177 -0
  63. package/src/pricing/estimator.test.ts +181 -0
  64. package/src/pricing/estimator.ts +80 -0
  65. package/src/pricing/fallback.test.ts +71 -0
  66. package/src/pricing/fallback.ts +62 -0
  67. package/src/pricing/index.ts +66 -0
  68. package/src/pricing/models-dev.ts +130 -0
  69. package/src/storage/database.ts +613 -0
  70. package/src/storage/db.ts +64 -0
  71. package/src/storage/index.ts +66 -0
  72. package/src/storage/migrations/index.ts +189 -0
  73. package/src/storage/paths.ts +24 -0
  74. package/src/storage/repos/agentSessions.ts +210 -0
  75. package/src/storage/repos/providerSnapshots.ts +116 -0
  76. package/src/storage/repos/usageEvents.ts +334 -0
  77. package/src/storage/types.ts +288 -0
  78. package/src/tui/App.tsx +381 -0
  79. package/src/tui/components/CommandPalette.tsx +166 -0
  80. package/src/tui/components/DebugConsole.tsx +161 -0
  81. package/src/tui/components/DebugPanel.tsx +660 -0
  82. package/src/tui/components/FooterHints.tsx +22 -0
  83. package/src/tui/components/GhostProviderCard.tsx +45 -0
  84. package/src/tui/components/Header.tsx +125 -0
  85. package/src/tui/components/HelpOverlay.tsx +45 -0
  86. package/src/tui/components/InlineGauge.tsx +34 -0
  87. package/src/tui/components/InlineSparkline.tsx +78 -0
  88. package/src/tui/components/KpiStrip.tsx +246 -0
  89. package/src/tui/components/LimitGauge.tsx +186 -0
  90. package/src/tui/components/ModalBackdrop.tsx +42 -0
  91. package/src/tui/components/ProviderAggregateStrip.tsx +200 -0
  92. package/src/tui/components/ProviderCard.tsx +309 -0
  93. package/src/tui/components/ProviderDetailPanel.tsx +237 -0
  94. package/src/tui/components/ProviderLimitsPanel.tsx +308 -0
  95. package/src/tui/components/ProvidersList.tsx +380 -0
  96. package/src/tui/components/SessionDetailsDrawer.tsx +317 -0
  97. package/src/tui/components/SessionsTable.tsx +401 -0
  98. package/src/tui/components/SettingsModal.tsx +587 -0
  99. package/src/tui/components/SidebarBreakdown.tsx +87 -0
  100. package/src/tui/components/Skeleton.tsx +102 -0
  101. package/src/tui/components/SmartSidebar.tsx +644 -0
  102. package/src/tui/components/Sparkline.tsx +180 -0
  103. package/src/tui/components/Spinner.tsx +36 -0
  104. package/src/tui/components/StatusBar.tsx +96 -0
  105. package/src/tui/components/ThemePicker.tsx +234 -0
  106. package/src/tui/components/Toast.tsx +61 -0
  107. package/src/tui/components/UsageGauge.tsx +80 -0
  108. package/src/tui/components/index.ts +5 -0
  109. package/src/tui/contexts/AgentSessionContext.tsx +307 -0
  110. package/src/tui/contexts/ConfigContext.tsx +141 -0
  111. package/src/tui/contexts/DashboardRuntimeContext.tsx +102 -0
  112. package/src/tui/contexts/DemoModeContext.tsx +60 -0
  113. package/src/tui/contexts/DrawerContext.tsx +42 -0
  114. package/src/tui/contexts/InputContext.tsx +30 -0
  115. package/src/tui/contexts/LogContext.tsx +150 -0
  116. package/src/tui/contexts/PluginContext.tsx +424 -0
  117. package/src/tui/contexts/RealTimeActivityContext.tsx +119 -0
  118. package/src/tui/contexts/StorageContext.tsx +233 -0
  119. package/src/tui/contexts/ThemeContext.tsx +95 -0
  120. package/src/tui/contexts/TimeWindowContext.tsx +158 -0
  121. package/src/tui/contexts/ToastContext.tsx +40 -0
  122. package/src/tui/contexts/index.ts +5 -0
  123. package/src/tui/createApp.tsx +47 -0
  124. package/src/tui/debug/captureFrame.ts +262 -0
  125. package/src/tui/debug/snapshot.tsx +644 -0
  126. package/src/tui/driver/assertions.ts +279 -0
  127. package/src/tui/driver/cli.ts +717 -0
  128. package/src/tui/driver/coverage.ts +204 -0
  129. package/src/tui/driver/demo-snapshot.ts +66 -0
  130. package/src/tui/driver/diff.ts +166 -0
  131. package/src/tui/driver/driver.ts +270 -0
  132. package/src/tui/driver/index.ts +53 -0
  133. package/src/tui/driver/recorder.ts +303 -0
  134. package/src/tui/driver/test-workflow.ts +115 -0
  135. package/src/tui/driver/test.ts +47 -0
  136. package/src/tui/hooks/index.ts +20 -0
  137. package/src/tui/hooks/useAnimatedValue.ts +113 -0
  138. package/src/tui/hooks/useDashboardKeyboard.ts +341 -0
  139. package/src/tui/hooks/useDashboardState.ts +116 -0
  140. package/src/tui/hooks/useEmaActivity.ts +223 -0
  141. package/src/tui/hooks/useEntranceAnimation.ts +62 -0
  142. package/src/tui/hooks/useExitAnimation.ts +137 -0
  143. package/src/tui/hooks/usePulse.ts +138 -0
  144. package/src/tui/hooks/useSafeRenderer.ts +24 -0
  145. package/src/tui/hooks/useValueFlash.ts +111 -0
  146. package/src/tui/index.tsx +50 -0
  147. package/src/tui/utils/providerColor.ts +32 -0
  148. package/src/tui/views/Dashboard.tsx +340 -0
  149. package/src/tui/views/HistoricalTrendsView.tsx +1431 -0
  150. package/src/tui/views/ProjectsView.tsx +1277 -0
  151. package/src/tui/views/RealTimeDashboard.tsx +393 -0
  152. package/src/tui/views/SettingsView.tsx +465 -0
  153. package/src/tui/views/index.ts +2 -0
  154. package/src/types/global.d.ts +5 -0
  155. package/src/types/opentui-test-utils.d.ts +18 -0
  156. package/src/utils/clipboard.ts +118 -0
  157. package/src/utils/currency.test.ts +106 -0
  158. package/src/utils/currency.ts +79 -0
  159. package/src/version.ts +1 -0
@@ -0,0 +1,125 @@
1
+ import type {
2
+ ProviderPlugin,
3
+ ProviderFetchContext,
4
+ ProviderUsageData,
5
+ ProviderAuth,
6
+ PluginContext,
7
+ CredentialResult,
8
+ Credentials,
9
+ } from '../types/provider.ts';
10
+
11
+ export const opencodeZenPlugin: ProviderPlugin = {
12
+ apiVersion: 2,
13
+ id: 'opencode-zen',
14
+ type: 'provider',
15
+ name: 'OpenCode Zen',
16
+ version: '1.0.0',
17
+
18
+ meta: {
19
+ description: 'OpenCode Zen - curated AI models for coding agents',
20
+ homepage: 'https://opencode.ai/zen',
21
+ brandColor: '#6366f1',
22
+ providerAliases: ['opencode'],
23
+ },
24
+
25
+ permissions: {
26
+ network: {
27
+ enabled: true,
28
+ allowedDomains: ['opencode.ai'],
29
+ },
30
+ filesystem: {
31
+ read: true,
32
+ paths: ['~/.local/share/opencode'],
33
+ },
34
+ env: {
35
+ read: true,
36
+ vars: ['OPENCODE_API_KEY'],
37
+ },
38
+ },
39
+
40
+ capabilities: {
41
+ usageLimits: false,
42
+ apiRateLimits: false,
43
+ tokenUsage: true,
44
+ actualCosts: true,
45
+ },
46
+
47
+ pricing: {
48
+ modelsDevProviderId: 'anthropic',
49
+ },
50
+
51
+ auth: {
52
+ async discover(ctx: PluginContext): Promise<CredentialResult> {
53
+ // 1. Try OpenCode auth (getProviderEntry reads auth.json and config)
54
+ const entry = await ctx.authSources.opencode.getProviderEntry('opencode');
55
+ if (entry) {
56
+ if (entry.type === 'api' && entry.key) {
57
+ return { ok: true, credentials: { apiKey: entry.key, source: 'opencode' } };
58
+ }
59
+ if (entry.type === 'wellknown' && (entry.token || entry.key)) {
60
+ return { ok: true, credentials: { apiKey: (entry.token || entry.key)!, source: 'opencode' } };
61
+ }
62
+ }
63
+
64
+ // 2. Try env vars
65
+ const apiKey = ctx.authSources.env.get('OPENCODE_API_KEY');
66
+ if (apiKey) {
67
+ return { ok: true, credentials: { apiKey, source: 'env' } };
68
+ }
69
+
70
+ return { ok: false, reason: 'missing', message: 'No OpenCode API key found. Run /connect in OpenCode to set up Zen.' };
71
+ },
72
+
73
+ isConfigured(credentials: Credentials): boolean {
74
+ return !!credentials.apiKey;
75
+ },
76
+ } satisfies ProviderAuth,
77
+
78
+ async fetchUsage(ctx: ProviderFetchContext): Promise<ProviderUsageData> {
79
+ const { credentials, http, logger: log } = ctx;
80
+
81
+ if (!credentials.apiKey) {
82
+ return {
83
+ fetchedAt: Date.now(),
84
+ error: 'API key required. Run /connect in OpenCode to set up Zen.',
85
+ };
86
+ }
87
+
88
+ log.info('OpenCode Zen configured', { keyPrefix: credentials.apiKey.slice(0, 10) });
89
+
90
+ try {
91
+ const response = await http.fetch('https://opencode.ai/zen/v1/models', {
92
+ headers: {
93
+ 'Authorization': `Bearer ${credentials.apiKey}`,
94
+ },
95
+ });
96
+
97
+ if (response.ok) {
98
+ log.info('OpenCode Zen API accessible');
99
+ return {
100
+ planType: 'Pay-as-you-go',
101
+ allowed: true,
102
+ fetchedAt: Date.now(),
103
+ credits: {
104
+ hasCredits: true,
105
+ unlimited: false,
106
+ },
107
+ };
108
+ }
109
+
110
+ return {
111
+ planType: 'Pay-as-you-go',
112
+ allowed: true,
113
+ fetchedAt: Date.now(),
114
+ };
115
+ } catch (err) {
116
+ log.warn('Could not verify OpenCode Zen API access', { error: String(err) });
117
+
118
+ return {
119
+ planType: 'Pay-as-you-go',
120
+ allowed: true,
121
+ fetchedAt: Date.now(),
122
+ };
123
+ }
124
+ },
125
+ };
@@ -0,0 +1,177 @@
1
+ import type {
2
+ ProviderPlugin,
3
+ ProviderFetchContext,
4
+ ProviderUsageData,
5
+ ProviderAuth,
6
+ PluginContext,
7
+ CredentialResult,
8
+ Credentials,
9
+ } from '../types/provider.ts';
10
+
11
+ /**
12
+ * Perplexity Shadow API Discovery (January 2026):
13
+ *
14
+ * Internal endpoints that power the billing dashboard (require session cookies, not API key):
15
+ * - GET /rest/pplx-api/v2/groups → { orgs: [{ api_org_id, display_name, usage_tier }] }
16
+ * - GET /rest/pplx-api/v2/groups/{orgId} → { customerInfo: { balance, auto_top_up_amount, ... } }
17
+ * - GET /rest/pplx-api/v2/groups/{orgId}/usage-analytics?time_bucket=day&time_range=past_month
18
+ * → Array of meter data (api_requests, input_tokens, output_tokens, etc.) with costs
19
+ * - GET /rest/pplx-api/v2/groups/{orgId}/invoices → Invoice history
20
+ *
21
+ * Unfortunately, these endpoints use web session auth, not API key auth.
22
+ * GitHub issue #266 requests a public usage API endpoint.
23
+ */
24
+
25
+ export const perplexityPlugin: ProviderPlugin = {
26
+ apiVersion: 2,
27
+ id: 'perplexity',
28
+ type: 'provider',
29
+ name: 'Perplexity',
30
+ version: '1.0.0',
31
+
32
+ meta: {
33
+ description: 'Perplexity AI API usage (credit-based)',
34
+ homepage: 'https://www.perplexity.ai',
35
+ brandColor: '#20b2aa',
36
+ },
37
+
38
+ permissions: {
39
+ network: {
40
+ enabled: true,
41
+ allowedDomains: ['api.perplexity.ai'],
42
+ },
43
+ env: {
44
+ read: true,
45
+ vars: ['PERPLEXITY_API_KEY'],
46
+ },
47
+ },
48
+
49
+ capabilities: {
50
+ usageLimits: false,
51
+ apiRateLimits: true,
52
+ tokenUsage: true,
53
+ actualCosts: false,
54
+ },
55
+
56
+ auth: {
57
+ async discover(ctx: PluginContext): Promise<CredentialResult> {
58
+ // 1. Try OpenCode auth first (wellknown type, has token or key)
59
+ const entry = await ctx.authSources.opencode.getProviderEntry('perplexity');
60
+ if (entry) {
61
+ if (entry.type === 'api' && entry.key) {
62
+ return { ok: true, credentials: { apiKey: entry.key, source: 'opencode' } };
63
+ }
64
+ if (entry.type === 'wellknown' && (entry.token || entry.key)) {
65
+ return { ok: true, credentials: { apiKey: (entry.token || entry.key)!, source: 'opencode' } };
66
+ }
67
+ }
68
+
69
+ // 2. Try env vars
70
+ const apiKey = ctx.authSources.env.get('PERPLEXITY_API_KEY');
71
+ if (apiKey) {
72
+ return { ok: true, credentials: { apiKey, source: 'env' } };
73
+ }
74
+
75
+ return { ok: false, reason: 'missing', message: 'No Perplexity API key found. Set PERPLEXITY_API_KEY or configure in OpenCode.' };
76
+ },
77
+
78
+ isConfigured(credentials: Credentials): boolean {
79
+ return !!credentials.apiKey;
80
+ },
81
+ } satisfies ProviderAuth,
82
+
83
+ async fetchUsage(ctx: ProviderFetchContext): Promise<ProviderUsageData> {
84
+ const { credentials, http, logger: log } = ctx;
85
+
86
+ if (!credentials.apiKey) {
87
+ return {
88
+ fetchedAt: Date.now(),
89
+ error: 'API key required. Set PERPLEXITY_API_KEY environment variable.',
90
+ };
91
+ }
92
+
93
+ log.info('Checking Perplexity API', { keyPrefix: credentials.apiKey.slice(0, 8) });
94
+
95
+ try {
96
+ const response = await http.fetch('https://api.perplexity.ai/chat/completions', {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Authorization': `Bearer ${credentials.apiKey}`,
100
+ 'Content-Type': 'application/json',
101
+ },
102
+ body: JSON.stringify({
103
+ model: 'sonar',
104
+ messages: [{ role: 'user', content: 'ping' }],
105
+ max_tokens: 1,
106
+ }),
107
+ });
108
+
109
+ if (response.status === 401) {
110
+ return {
111
+ fetchedAt: Date.now(),
112
+ error: 'Invalid API key',
113
+ };
114
+ }
115
+
116
+ if (response.status === 402) {
117
+ return {
118
+ planType: 'API',
119
+ allowed: false,
120
+ fetchedAt: Date.now(),
121
+ credits: {
122
+ hasCredits: false,
123
+ unlimited: false,
124
+ balance: '$0.00',
125
+ },
126
+ error: 'Insufficient credits',
127
+ };
128
+ }
129
+
130
+ if (response.status === 429) {
131
+ return {
132
+ planType: 'API',
133
+ allowed: true,
134
+ fetchedAt: Date.now(),
135
+ error: 'Rate limited',
136
+ };
137
+ }
138
+
139
+ if (response.ok) {
140
+ const data = await response.json().catch(() => null) as { usage?: { prompt_tokens?: number; completion_tokens?: number } } | null;
141
+ const usage = data?.usage;
142
+
143
+ return {
144
+ planType: 'API',
145
+ allowed: true,
146
+ fetchedAt: Date.now(),
147
+ credits: {
148
+ hasCredits: true,
149
+ unlimited: false,
150
+ },
151
+ ...(usage && {
152
+ tokens: {
153
+ input: usage.prompt_tokens ?? 0,
154
+ output: usage.completion_tokens ?? 0,
155
+ },
156
+ }),
157
+ };
158
+ }
159
+
160
+ const errorText = await response.text().catch(() => '');
161
+ return {
162
+ planType: 'API',
163
+ allowed: true,
164
+ fetchedAt: Date.now(),
165
+ error: `API error: ${response.status} ${errorText.slice(0, 100)}`,
166
+ };
167
+ } catch (err) {
168
+ const msg = err instanceof Error ? err.message : String(err);
169
+ log.error('Perplexity fetch error', { error: msg });
170
+
171
+ return {
172
+ fetchedAt: Date.now(),
173
+ error: `Connection error: ${msg}`,
174
+ };
175
+ }
176
+ },
177
+ };
@@ -0,0 +1,225 @@
1
+ import type {
2
+ ProviderPlugin,
3
+ ProviderFetchContext,
4
+ ProviderUsageData,
5
+ UsageLimit,
6
+ ProviderAuth,
7
+ PluginContext,
8
+ CredentialResult,
9
+ Credentials,
10
+ } from '../types/provider.ts';
11
+
12
+ const ZAI_USAGE_URL = 'https://api.z.ai/api/monitor/usage/quota/limit';
13
+
14
+ interface ZaiLimitEntry {
15
+ type?: string;
16
+ usage?: number;
17
+ currentValue?: number;
18
+ remaining?: number;
19
+ percentage?: number;
20
+ }
21
+
22
+ interface ZaiUsageResponse {
23
+ success?: boolean;
24
+ data?: {
25
+ limits?: ZaiLimitEntry[];
26
+ };
27
+ }
28
+
29
+ export const zaiCodingPlanPlugin: ProviderPlugin = {
30
+ apiVersion: 2,
31
+ id: 'zai-coding-plan',
32
+ type: 'provider',
33
+ name: 'Z.ai Coding Plan',
34
+ version: '1.0.0',
35
+
36
+ meta: {
37
+ description: 'Z.ai coding plan usage and quota tracking',
38
+ homepage: 'https://z.ai',
39
+ brandColor: '#10b981',
40
+ },
41
+
42
+ permissions: {
43
+ network: {
44
+ enabled: true,
45
+ allowedDomains: ['api.z.ai'],
46
+ },
47
+ filesystem: {
48
+ read: true,
49
+ paths: ['~/.local/share/opencode'],
50
+ },
51
+ env: {
52
+ read: true,
53
+ vars: ['ZAI_API_KEY'],
54
+ },
55
+ },
56
+
57
+ capabilities: {
58
+ usageLimits: true,
59
+ apiRateLimits: false,
60
+ tokenUsage: false,
61
+ actualCosts: false,
62
+ },
63
+
64
+ auth: {
65
+ async discover(ctx: PluginContext): Promise<CredentialResult> {
66
+ // 1. Try OpenCode auth (api, wellknown, or oauth)
67
+ const entry = await ctx.authSources.opencode.getProviderEntry('zai-coding-plan');
68
+ if (entry) {
69
+ if (entry.type === 'api' && entry.key) {
70
+ return { ok: true, credentials: { apiKey: entry.key, source: 'opencode' } };
71
+ }
72
+ if (entry.type === 'wellknown' && (entry.token || entry.key)) {
73
+ return { ok: true, credentials: { apiKey: (entry.token || entry.key)!, source: 'opencode' } };
74
+ }
75
+ if (entry.type === 'oauth' && entry.access) {
76
+ return { ok: true, credentials: { apiKey: entry.access, source: 'opencode' } };
77
+ }
78
+ }
79
+
80
+ // 2. Try env vars
81
+ const apiKey = ctx.authSources.env.get('ZAI_API_KEY');
82
+ if (apiKey) {
83
+ return { ok: true, credentials: { apiKey, source: 'env' } };
84
+ }
85
+
86
+ return { ok: false, reason: 'missing', message: 'No Z.ai API key found. Configure Z.ai in OpenCode.' };
87
+ },
88
+
89
+ isConfigured(credentials: Credentials): boolean {
90
+ return !!(credentials.apiKey || credentials.oauth?.accessToken);
91
+ },
92
+ } satisfies ProviderAuth,
93
+
94
+ async fetchUsage(ctx: ProviderFetchContext): Promise<ProviderUsageData> {
95
+ const { credentials, http, logger: log } = ctx;
96
+
97
+ const token = credentials.apiKey || credentials.oauth?.accessToken;
98
+
99
+ if (!token) {
100
+ return {
101
+ fetchedAt: Date.now(),
102
+ error: 'API key required. Configure Z.ai in OpenCode.',
103
+ };
104
+ }
105
+
106
+ try {
107
+ const response = await http.fetch(ZAI_USAGE_URL, {
108
+ method: 'GET',
109
+ headers: {
110
+ 'Authorization': `Bearer ${token}`,
111
+ 'Accept': 'application/json',
112
+ },
113
+ });
114
+
115
+ if (!response.ok) {
116
+ log.warn('Failed to fetch Z.ai usage', { status: response.status });
117
+
118
+ if (response.status === 401 || response.status === 403) {
119
+ return {
120
+ fetchedAt: Date.now(),
121
+ error: 'Authorization failed. Check your Z.ai API key.',
122
+ };
123
+ }
124
+
125
+ return {
126
+ fetchedAt: Date.now(),
127
+ error: `API error: ${response.status} ${response.statusText}`,
128
+ };
129
+ }
130
+
131
+ const data = (await response.json()) as ZaiUsageResponse;
132
+
133
+ if (!data || data.success !== true) {
134
+ return {
135
+ fetchedAt: Date.now(),
136
+ planType: 'Z.ai',
137
+ allowed: true,
138
+ };
139
+ }
140
+
141
+ const limits = data.data?.limits ?? [];
142
+ if (limits.length === 0) {
143
+ return {
144
+ fetchedAt: Date.now(),
145
+ planType: 'Z.ai',
146
+ allowed: true,
147
+ };
148
+ }
149
+
150
+ const parsedLimits = limits
151
+ .map(limit => ({
152
+ type: limit.type,
153
+ usedPercent: resolveUsedPercent(limit),
154
+ remaining: limit.remaining,
155
+ }))
156
+ .filter(l => l.usedPercent !== null);
157
+
158
+ let primary: UsageLimit | undefined;
159
+ for (const limit of parsedLimits) {
160
+ if (limit.usedPercent === null) continue;
161
+ if (!primary || limit.usedPercent > (primary.usedPercent ?? 0)) {
162
+ primary = {
163
+ usedPercent: limit.usedPercent,
164
+ label: formatLimitType(limit.type),
165
+ };
166
+ }
167
+ }
168
+
169
+ const limitReached = parsedLimits.some(l =>
170
+ (l.usedPercent !== null && l.usedPercent >= 100) ||
171
+ (typeof l.remaining === 'number' && l.remaining <= 0)
172
+ );
173
+
174
+ const result: ProviderUsageData = {
175
+ planType: 'Z.ai Coding Plan',
176
+ allowed: !limitReached,
177
+ limitReached,
178
+ fetchedAt: Date.now(),
179
+ };
180
+
181
+ if (primary) {
182
+ result.limits = { primary };
183
+ }
184
+
185
+ return result;
186
+ } catch (err) {
187
+ log.error('Failed to fetch Z.ai usage', { error: err });
188
+ return {
189
+ fetchedAt: Date.now(),
190
+ error: err instanceof Error ? err.message : 'Unknown error',
191
+ };
192
+ }
193
+ },
194
+ };
195
+
196
+ function resolveUsedPercent(limit: ZaiLimitEntry): number | null {
197
+ if (typeof limit.percentage === 'number') {
198
+ return Math.round(Math.max(0, Math.min(100, limit.percentage)));
199
+ }
200
+
201
+ const total = limit.usage;
202
+ const used = typeof limit.currentValue === 'number'
203
+ ? limit.currentValue
204
+ : (total !== undefined && typeof limit.remaining === 'number')
205
+ ? Math.max(0, total - limit.remaining)
206
+ : undefined;
207
+
208
+ if (total && used !== undefined && total > 0) {
209
+ return Math.round((used / total) * 100);
210
+ }
211
+
212
+ return null;
213
+ }
214
+
215
+ function formatLimitType(type?: string): string {
216
+ if (!type) return 'Quota';
217
+ switch (type) {
218
+ case 'TIME_LIMIT':
219
+ return 'Time';
220
+ case 'TOKENS_LIMIT':
221
+ return 'Tokens';
222
+ default:
223
+ return type.replace(/_/g, ' ').toLowerCase();
224
+ }
225
+ }