@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,194 @@
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
+ interface CopilotUserResponse {
13
+ access_type_sku?: string;
14
+ copilot_plan?: string;
15
+ chat_enabled?: boolean;
16
+ quota_reset_date?: string;
17
+ quota_reset_date_utc?: string;
18
+ quota_snapshots?: Record<string, CopilotQuotaSnapshot | undefined>;
19
+ }
20
+
21
+ interface CopilotQuotaSnapshot {
22
+ entitlement: number;
23
+ remaining: number;
24
+ percent_remaining?: number;
25
+ unlimited: boolean;
26
+ quota_id?: string;
27
+ timestamp_utc?: string;
28
+ }
29
+
30
+ export const githubCopilotPlugin: ProviderPlugin = {
31
+ apiVersion: 2,
32
+ id: 'github-copilot',
33
+ type: 'provider',
34
+ name: 'GitHub Copilot',
35
+ version: '1.0.0',
36
+
37
+ meta: {
38
+ description: 'GitHub Copilot usage tracking including premium requests',
39
+ homepage: 'https://github.com/features/copilot',
40
+ brandColor: '#6e40c9',
41
+ },
42
+
43
+ permissions: {
44
+ network: {
45
+ enabled: true,
46
+ allowedDomains: ['api.github.com'],
47
+ },
48
+ env: {
49
+ read: true,
50
+ vars: ['GITHUB_TOKEN', 'GH_TOKEN'],
51
+ },
52
+ filesystem: {
53
+ read: true,
54
+ paths: ['~/.config/github-copilot'],
55
+ },
56
+ },
57
+
58
+ capabilities: {
59
+ usageLimits: true,
60
+ apiRateLimits: false,
61
+ tokenUsage: false,
62
+ actualCosts: false,
63
+ },
64
+
65
+ pricing: {
66
+ modelsDevProviderId: 'openai',
67
+ },
68
+
69
+ auth: {
70
+ async discover(ctx: PluginContext): Promise<CredentialResult> {
71
+ const entry = await ctx.authSources.opencode.getProviderEntry('github-copilot');
72
+ if (entry) {
73
+ const token = entry.token || entry.key || entry.access;
74
+ if (token) {
75
+ return { ok: true, credentials: { apiKey: token, source: 'opencode' } };
76
+ }
77
+ }
78
+
79
+ // 2. Try env vars
80
+ const githubToken = ctx.authSources.env.get('GITHUB_TOKEN');
81
+ if (githubToken) {
82
+ return { ok: true, credentials: { apiKey: githubToken, source: 'env' } };
83
+ }
84
+
85
+ const ghToken = ctx.authSources.env.get('GH_TOKEN');
86
+ if (ghToken) {
87
+ return { ok: true, credentials: { apiKey: ghToken, source: 'env' } };
88
+ }
89
+
90
+ // 3. Try external file: ~/.config/github-copilot/hosts.json
91
+ const hostsData = await ctx.authSources.files.readJson<{
92
+ 'github.com'?: { oauth_token?: string; user?: string };
93
+ }>('~/.config/github-copilot/hosts.json');
94
+ if (hostsData) {
95
+ const githubEntry = hostsData['github.com'];
96
+ if (githubEntry?.oauth_token) {
97
+ return { ok: true, credentials: { apiKey: githubEntry.oauth_token, source: 'external' } };
98
+ }
99
+ }
100
+
101
+ return { ok: false, reason: 'missing', message: 'No GitHub token found. Set GITHUB_TOKEN or install GitHub Copilot CLI.' };
102
+ },
103
+
104
+ isConfigured(credentials: Credentials): boolean {
105
+ return !!(credentials.apiKey || credentials.oauth?.accessToken);
106
+ },
107
+ } satisfies ProviderAuth,
108
+
109
+ async fetchUsage(ctx: ProviderFetchContext): Promise<ProviderUsageData> {
110
+ const { credentials, http, logger: log } = ctx;
111
+ const token = credentials.apiKey ?? credentials.oauth?.accessToken;
112
+
113
+ if (!token) {
114
+ return {
115
+ fetchedAt: Date.now(),
116
+ error: 'Not configured: missing GitHub token',
117
+ };
118
+ }
119
+
120
+ const headers: Record<string, string> = {
121
+ 'Accept': 'application/vnd.github+json',
122
+ 'Authorization': `token ${token}`,
123
+ 'X-GitHub-Api-Version': '2022-11-28',
124
+ };
125
+
126
+ try {
127
+ const response = await http.fetch('https://api.github.com/copilot_internal/user', {
128
+ method: 'GET',
129
+ headers,
130
+ });
131
+
132
+ if (!response.ok) {
133
+ if (response.status === 404) {
134
+ return {
135
+ planType: 'None',
136
+ fetchedAt: Date.now(),
137
+ error: 'Copilot not enabled for this account',
138
+ };
139
+ }
140
+ log.warn('Failed to fetch Copilot usage', { status: response.status });
141
+ return {
142
+ fetchedAt: Date.now(),
143
+ error: `API error: ${response.status} ${response.statusText}`,
144
+ };
145
+ }
146
+
147
+ const data = (await response.json()) as CopilotUserResponse;
148
+
149
+ const planType = data.copilot_plan ?? data.access_type_sku ?? 'Copilot';
150
+ const result: ProviderUsageData = {
151
+ planType,
152
+ fetchedAt: Date.now(),
153
+ };
154
+
155
+ const premiumSnapshot = data.quota_snapshots?.premium_interactions;
156
+ if (premiumSnapshot) {
157
+ const used = premiumSnapshot.entitlement - premiumSnapshot.remaining;
158
+ const usedPercent = premiumSnapshot.unlimited
159
+ ? 0
160
+ : premiumSnapshot.percent_remaining !== undefined
161
+ ? Math.max(0, 100 - premiumSnapshot.percent_remaining)
162
+ : premiumSnapshot.entitlement > 0
163
+ ? (used / premiumSnapshot.entitlement) * 100
164
+ : 0;
165
+
166
+ const label = premiumSnapshot.unlimited
167
+ ? 'Premium requests (unlimited)'
168
+ : `Premium requests (${Math.max(0, used)}/${premiumSnapshot.entitlement})`;
169
+
170
+ const primary: UsageLimit = {
171
+ usedPercent,
172
+ label,
173
+ };
174
+
175
+ const resetAt = data.quota_reset_date_utc ?? data.quota_reset_date;
176
+ if (resetAt) {
177
+ primary.resetsAt = new Date(resetAt).getTime();
178
+ }
179
+
180
+ result.limits = { primary };
181
+
182
+ result.limitReached = !premiumSnapshot.unlimited && premiumSnapshot.remaining <= 0;
183
+ }
184
+
185
+ return result;
186
+ } catch (err) {
187
+ log.error('Failed to fetch Copilot usage', { error: err });
188
+ return {
189
+ fetchedAt: Date.now(),
190
+ error: err instanceof Error ? err.message : 'Unknown error',
191
+ };
192
+ }
193
+ },
194
+ };
@@ -0,0 +1,12 @@
1
+ export { anthropicPlugin } from './anthropic.ts';
2
+ export { codexPlugin } from './codex.ts';
3
+ export { antigravityPlugin } from './antigravity.ts';
4
+ export { githubCopilotPlugin } from './github-copilot.ts';
5
+ export { geminiPlugin } from './gemini.ts';
6
+
7
+ export { openaiApiPlugin } from './openai-api.ts';
8
+ export { perplexityPlugin } from './perplexity.ts';
9
+ export { opencodeZenPlugin } from './opencode-zen.ts';
10
+
11
+ export { zaiCodingPlanPlugin } from './zai.ts';
12
+ export { minimaxPlugin, minimaxCodingPlanPlugin } from './minimax.ts';
@@ -0,0 +1,223 @@
1
+ import type {
2
+ ProviderPlugin,
3
+ ProviderFetchContext,
4
+ ProviderUsageData,
5
+ ProviderAuth,
6
+ PluginContext,
7
+ CredentialResult,
8
+ Credentials,
9
+ } from '../types/provider.ts';
10
+
11
+ const MINIMAX_USAGE_URL = 'https://platform.minimax.io/v1/api/openplatform/coding_plan/remains';
12
+
13
+ interface MinimaxModelRemain {
14
+ model_name?: string;
15
+ current_interval_total_count?: number;
16
+ current_interval_usage_count?: number;
17
+ remains_time?: number;
18
+ }
19
+
20
+ interface MinimaxUsageResponse {
21
+ base_resp?: {
22
+ status_code?: number;
23
+ status_msg?: string;
24
+ };
25
+ model_remains?: MinimaxModelRemain[];
26
+ }
27
+
28
+ function createMinimaxPlugin(id: string, name: string): ProviderPlugin {
29
+ return {
30
+ apiVersion: 2,
31
+ id,
32
+ type: 'provider',
33
+ name,
34
+ version: '1.0.0',
35
+
36
+ meta: {
37
+ description: `${name} usage and quota tracking`,
38
+ homepage: 'https://platform.minimax.io',
39
+ brandColor: '#6366f1',
40
+ },
41
+
42
+ permissions: {
43
+ network: {
44
+ enabled: true,
45
+ allowedDomains: ['platform.minimax.io'],
46
+ },
47
+ filesystem: {
48
+ read: true,
49
+ paths: ['~/.local/share/opencode'],
50
+ },
51
+ env: {
52
+ read: true,
53
+ vars: ['MINIMAX_API_KEY', 'MINIMAX_GROUP_ID'],
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 type with key + groupId)
67
+ const entry = await ctx.authSources.opencode.getProviderEntry(id);
68
+ if (entry) {
69
+ if (entry.type === 'api' && entry.key) {
70
+ return {
71
+ ok: true,
72
+ credentials: {
73
+ apiKey: entry.key,
74
+ ...(entry.groupId && { groupId: entry.groupId }),
75
+ source: 'opencode',
76
+ },
77
+ };
78
+ }
79
+ if (entry.type === 'wellknown' && (entry.token || entry.key)) {
80
+ return {
81
+ ok: true,
82
+ credentials: {
83
+ apiKey: (entry.token || entry.key)!,
84
+ ...(entry.groupId && { groupId: entry.groupId }),
85
+ source: 'opencode',
86
+ },
87
+ };
88
+ }
89
+ }
90
+
91
+ // 2. Try env vars
92
+ const apiKey = ctx.authSources.env.get('MINIMAX_API_KEY');
93
+ if (apiKey) {
94
+ const groupId = ctx.authSources.env.get('MINIMAX_GROUP_ID');
95
+ return {
96
+ ok: true,
97
+ credentials: {
98
+ apiKey,
99
+ ...(groupId && { groupId }),
100
+ source: 'env',
101
+ },
102
+ };
103
+ }
104
+
105
+ return { ok: false, reason: 'missing', message: 'No MiniMax API key found. Configure MiniMax in OpenCode.' };
106
+ },
107
+
108
+ isConfigured(credentials: Credentials): boolean {
109
+ return !!credentials.apiKey;
110
+ },
111
+ } satisfies ProviderAuth,
112
+
113
+ async fetchUsage(ctx: ProviderFetchContext): Promise<ProviderUsageData> {
114
+ const { credentials, http, logger: log, config } = ctx;
115
+
116
+ const apiKey = credentials.apiKey;
117
+ if (!apiKey) {
118
+ return {
119
+ fetchedAt: Date.now(),
120
+ error: 'API key required. Configure MiniMax in OpenCode.',
121
+ };
122
+ }
123
+
124
+ const groupId = credentials.groupId ||
125
+ (config.groupId as string | undefined) ||
126
+ process.env.MINIMAX_GROUP_ID;
127
+
128
+ if (!groupId) {
129
+ return {
130
+ fetchedAt: Date.now(),
131
+ error: 'MiniMax groupId required. Set MINIMAX_GROUP_ID or configure in OpenCode.',
132
+ };
133
+ }
134
+
135
+ try {
136
+ const url = `${MINIMAX_USAGE_URL}?GroupId=${encodeURIComponent(groupId)}`;
137
+ const response = await http.fetch(url, {
138
+ method: 'GET',
139
+ headers: {
140
+ 'Authorization': `Bearer ${apiKey}`,
141
+ 'Referer': 'https://platform.minimax.io/user-center/payment/coding-plan',
142
+ 'Accept': 'application/json',
143
+ },
144
+ });
145
+
146
+ if (!response.ok) {
147
+ log.warn('Failed to fetch MiniMax usage', { status: response.status });
148
+
149
+ if (response.status === 401 || response.status === 403) {
150
+ return {
151
+ fetchedAt: Date.now(),
152
+ error: 'Authorization failed. Check your MiniMax API key.',
153
+ };
154
+ }
155
+
156
+ return {
157
+ fetchedAt: Date.now(),
158
+ error: `API error: ${response.status} ${response.statusText}`,
159
+ };
160
+ }
161
+
162
+ const data = (await response.json()) as MinimaxUsageResponse;
163
+
164
+ if (!data || data.base_resp?.status_code !== 0) {
165
+ const errorMsg = data?.base_resp?.status_msg || 'Unknown error';
166
+ return {
167
+ fetchedAt: Date.now(),
168
+ error: `MiniMax API error: ${errorMsg}`,
169
+ };
170
+ }
171
+
172
+ const modelRemains = data.model_remains ?? [];
173
+ if (modelRemains.length === 0) {
174
+ return {
175
+ fetchedAt: Date.now(),
176
+ planType: 'MiniMax Coding Plan',
177
+ allowed: true,
178
+ };
179
+ }
180
+
181
+ const model = modelRemains[0]!;
182
+ const totalCount = model.current_interval_total_count ?? 0;
183
+ const remainingCount = model.current_interval_usage_count ?? 0;
184
+ const remainsTime = model.remains_time ?? 0;
185
+
186
+ const usedCount = totalCount > 0 ? totalCount - remainingCount : 0;
187
+ const usedPercent = totalCount > 0
188
+ ? Math.round((usedCount / totalCount) * 100)
189
+ : 0;
190
+
191
+ const limitReached = usedCount >= totalCount && totalCount > 0;
192
+
193
+ const result: ProviderUsageData = {
194
+ planType: model.model_name || 'MiniMax Coding Plan',
195
+ allowed: !limitReached,
196
+ limitReached,
197
+ limits: {
198
+ primary: {
199
+ usedPercent,
200
+ label: 'Requests',
201
+ },
202
+ },
203
+ fetchedAt: Date.now(),
204
+ };
205
+
206
+ if (remainsTime > 0 && result.limits?.primary) {
207
+ result.limits.primary.resetsAt = Date.now() + remainsTime;
208
+ }
209
+
210
+ return result;
211
+ } catch (err) {
212
+ log.error('Failed to fetch MiniMax usage', { error: err });
213
+ return {
214
+ fetchedAt: Date.now(),
215
+ error: err instanceof Error ? err.message : 'Unknown error',
216
+ };
217
+ }
218
+ },
219
+ };
220
+ }
221
+
222
+ export const minimaxPlugin = createMinimaxPlugin('minimax', 'MiniMax');
223
+ export const minimaxCodingPlanPlugin = createMinimaxPlugin('minimax-coding-plan', 'MiniMax Coding Plan');
@@ -0,0 +1,250 @@
1
+ import type {
2
+ ProviderPlugin,
3
+ ProviderFetchContext,
4
+ ProviderUsageData,
5
+ ProviderAuth,
6
+ PluginContext,
7
+ CredentialResult,
8
+ Credentials,
9
+ } from '../types/provider.ts';
10
+
11
+ interface UsageBucket {
12
+ object: string;
13
+ start_time: number;
14
+ end_time: number;
15
+ results: Array<{
16
+ object: string;
17
+ input_tokens: number;
18
+ output_tokens: number;
19
+ input_cached_tokens?: number;
20
+ num_model_requests: number;
21
+ model?: string | null;
22
+ }>;
23
+ }
24
+
25
+ interface UsageResponse {
26
+ object: string;
27
+ data: UsageBucket[];
28
+ has_more: boolean;
29
+ }
30
+
31
+ interface CostsBucket {
32
+ object: string;
33
+ start_time: number;
34
+ end_time: number;
35
+ results: Array<{
36
+ object: string;
37
+ amount: {
38
+ value: number;
39
+ currency: string;
40
+ };
41
+ line_item?: string | null;
42
+ project_id?: string | null;
43
+ }>;
44
+ }
45
+
46
+ interface CostsResponse {
47
+ object: string;
48
+ data: CostsBucket[];
49
+ has_more: boolean;
50
+ }
51
+
52
+ export const openaiApiPlugin: ProviderPlugin = {
53
+ apiVersion: 2,
54
+ id: 'openai-api',
55
+ type: 'provider',
56
+ name: 'OpenAI API',
57
+ version: '1.0.0',
58
+
59
+ meta: {
60
+ description: 'OpenAI Codex subscription usage tracking (OAuth)',
61
+ homepage: 'https://openai.com/codex',
62
+ brandColor: '#10a37f',
63
+ providerAliases: ['openai'],
64
+ },
65
+
66
+ permissions: {
67
+ network: {
68
+ enabled: true,
69
+ allowedDomains: ['api.openai.com'],
70
+ },
71
+ env: {
72
+ read: true,
73
+ vars: ['OPENAI_API_KEY', 'OPENAI_ADMIN_KEY'],
74
+ },
75
+ },
76
+
77
+ capabilities: {
78
+ usageLimits: false,
79
+ apiRateLimits: true,
80
+ tokenUsage: true,
81
+ actualCosts: true,
82
+ },
83
+
84
+ pricing: {
85
+ modelsDevProviderId: 'openai',
86
+ },
87
+
88
+ auth: {
89
+ async discover(ctx: PluginContext): Promise<CredentialResult> {
90
+ const entry = await ctx.authSources.opencode.getProviderEntry('openai');
91
+ if (entry) {
92
+ if (entry.type === 'api' && entry.key) {
93
+ return { ok: true, credentials: { apiKey: entry.key, source: 'opencode' } };
94
+ }
95
+ if (entry.type === 'wellknown' && (entry.token || entry.key)) {
96
+ return { ok: true, credentials: { apiKey: (entry.token || entry.key)!, source: 'opencode' } };
97
+ }
98
+ }
99
+
100
+ // 2. Try env vars (OPENAI_API_KEY, then OPENAI_ADMIN_KEY)
101
+ const apiKey = ctx.authSources.env.get('OPENAI_API_KEY');
102
+ if (apiKey) {
103
+ return { ok: true, credentials: { apiKey, source: 'env' } };
104
+ }
105
+
106
+ const adminKey = ctx.authSources.env.get('OPENAI_ADMIN_KEY');
107
+ if (adminKey) {
108
+ return { ok: true, credentials: { apiKey: adminKey, source: 'env' } };
109
+ }
110
+
111
+ return { ok: false, reason: 'missing', message: 'No OpenAI API key found. Set OPENAI_API_KEY or configure in OpenCode.' };
112
+ },
113
+
114
+ isConfigured(credentials: Credentials): boolean {
115
+ return !!credentials.apiKey;
116
+ },
117
+ } satisfies ProviderAuth,
118
+
119
+ async fetchUsage(ctx: ProviderFetchContext): Promise<ProviderUsageData> {
120
+ const { credentials, http, logger: log } = ctx;
121
+
122
+ if (!credentials.apiKey) {
123
+ return {
124
+ fetchedAt: Date.now(),
125
+ error: 'API key required. Set OPENAI_API_KEY environment variable.',
126
+ };
127
+ }
128
+
129
+ const adminKey = process.env.OPENAI_ADMIN_KEY || credentials.apiKey;
130
+ const now = Math.floor(Date.now() / 1000);
131
+ const oneDayAgo = now - 86400;
132
+
133
+ log.info('Fetching OpenAI usage', { keyPrefix: credentials.apiKey.slice(0, 10) });
134
+
135
+ try {
136
+ const [usageResult, costsResult] = await Promise.allSettled([
137
+ fetchCompletionsUsage(http, adminKey, oneDayAgo),
138
+ fetchCosts(http, adminKey, oneDayAgo),
139
+ ]);
140
+
141
+ const result: ProviderUsageData = {
142
+ planType: 'API',
143
+ allowed: true,
144
+ fetchedAt: Date.now(),
145
+ };
146
+
147
+ if (usageResult.status === 'fulfilled' && usageResult.value) {
148
+ const usage = usageResult.value;
149
+ const totalInput = usage.data.reduce((sum, bucket) =>
150
+ sum + bucket.results.reduce((s, r) => s + (r.input_tokens || 0), 0), 0);
151
+ const totalOutput = usage.data.reduce((sum, bucket) =>
152
+ sum + bucket.results.reduce((s, r) => s + (r.output_tokens || 0), 0), 0);
153
+ const totalCached = usage.data.reduce((sum, bucket) =>
154
+ sum + bucket.results.reduce((s, r) => s + (r.input_cached_tokens || 0), 0), 0);
155
+
156
+ result.tokens = {
157
+ input: totalInput,
158
+ output: totalOutput,
159
+ cacheRead: totalCached,
160
+ };
161
+
162
+ log.debug('OpenAI tokens (24h)', { input: totalInput, output: totalOutput, cached: totalCached });
163
+ } else if (usageResult.status === 'rejected') {
164
+ log.warn('Failed to fetch OpenAI usage', { error: usageResult.reason?.message });
165
+ }
166
+
167
+ if (costsResult.status === 'fulfilled' && costsResult.value) {
168
+ const costs = costsResult.value;
169
+ const totalCost = costs.data.reduce((sum, bucket) =>
170
+ sum + bucket.results.reduce((s, r) => s + (r.amount?.value || 0), 0), 0);
171
+
172
+ const currency = costs.data[0]?.results[0]?.amount?.currency || 'USD';
173
+
174
+ if (totalCost > 0) {
175
+ result.cost = {
176
+ actual: {
177
+ total: totalCost,
178
+ currency,
179
+ },
180
+ source: 'api',
181
+ };
182
+ }
183
+
184
+ log.debug('OpenAI costs (24h)', { total: totalCost, currency });
185
+ } else if (costsResult.status === 'rejected') {
186
+ log.warn('Failed to fetch OpenAI costs', { error: costsResult.reason?.message });
187
+ }
188
+
189
+ return result;
190
+ } catch (err) {
191
+ const msg = err instanceof Error ? err.message : String(err);
192
+ log.error('OpenAI fetch error', { error: msg });
193
+
194
+ return {
195
+ planType: 'API',
196
+ allowed: true,
197
+ fetchedAt: Date.now(),
198
+ error: `Failed to fetch usage: ${msg}`,
199
+ };
200
+ }
201
+ },
202
+ };
203
+
204
+ async function fetchCompletionsUsage(
205
+ http: ProviderFetchContext['http'],
206
+ apiKey: string,
207
+ startTime: number
208
+ ): Promise<UsageResponse | null> {
209
+ const url = `https://api.openai.com/v1/organization/usage/completions?start_time=${startTime}&bucket_width=1d&limit=1`;
210
+
211
+ const response = await http.fetch(url, {
212
+ headers: {
213
+ 'Authorization': `Bearer ${apiKey}`,
214
+ 'Content-Type': 'application/json',
215
+ },
216
+ });
217
+
218
+ if (!response.ok) {
219
+ if (response.status === 403) {
220
+ return null;
221
+ }
222
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
223
+ }
224
+
225
+ return response.json() as Promise<UsageResponse>;
226
+ }
227
+
228
+ async function fetchCosts(
229
+ http: ProviderFetchContext['http'],
230
+ apiKey: string,
231
+ startTime: number
232
+ ): Promise<CostsResponse | null> {
233
+ const url = `https://api.openai.com/v1/organization/usage/costs?start_time=${startTime}&bucket_width=1d&limit=1`;
234
+
235
+ const response = await http.fetch(url, {
236
+ headers: {
237
+ 'Authorization': `Bearer ${apiKey}`,
238
+ 'Content-Type': 'application/json',
239
+ },
240
+ });
241
+
242
+ if (!response.ok) {
243
+ if (response.status === 403) {
244
+ return null;
245
+ }
246
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
247
+ }
248
+
249
+ return response.json() as Promise<CostsResponse>;
250
+ }