opencode-kimi-rotator 1.0.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/src/plugin.ts ADDED
@@ -0,0 +1,176 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ import { KimiAccountManager } from './accounts.js';
3
+ import type { KimiAccount } from './types.js';
4
+
5
+ interface OpenCodeAuth {
6
+ type: 'api';
7
+ key: string;
8
+ }
9
+
10
+ let accountManager: KimiAccountManager | null = null;
11
+ let currentAccountIndex: number = 0;
12
+ let originalFetch: typeof globalThis.fetch | null = null;
13
+
14
+ async function getAccountManager(): Promise<KimiAccountManager> {
15
+ if (!accountManager) {
16
+ accountManager = new KimiAccountManager();
17
+ await accountManager.init();
18
+ }
19
+ return accountManager;
20
+ }
21
+
22
+ async function getAuth(): Promise<OpenCodeAuth | null> {
23
+ const manager = await getAccountManager();
24
+ const result = await manager.getNextAccount();
25
+
26
+ if (!result) {
27
+ return null;
28
+ }
29
+
30
+ currentAccountIndex = result.index;
31
+
32
+ return {
33
+ type: 'api',
34
+ key: result.account.key,
35
+ };
36
+ }
37
+
38
+ let openCodeClient: {
39
+ tui?: {
40
+ showToast: (options: { body: { message: string; variant: 'info' | 'warning' | 'error' } }) => Promise<unknown>;
41
+ };
42
+ } | null = null;
43
+
44
+ let toastQueue: Array<{ message: string; variant: 'info' | 'warning' | 'error' }> = [];
45
+
46
+ async function showToast(message: string, variant: 'info' | 'warning' | 'error' = 'info') {
47
+ if (!openCodeClient?.tui?.showToast) {
48
+ toastQueue.push({ message, variant });
49
+ return;
50
+ }
51
+ try {
52
+ await openCodeClient.tui.showToast({ body: { message, variant } });
53
+ } catch {
54
+ return;
55
+ }
56
+ }
57
+
58
+ async function flushToastQueue() {
59
+ if (!openCodeClient?.tui?.showToast) return;
60
+ while (toastQueue.length > 0) {
61
+ const toast = toastQueue.shift();
62
+ if (toast) {
63
+ try {
64
+ await openCodeClient.tui.showToast({ body: { message: toast.message, variant: toast.variant } });
65
+ } catch {
66
+ return;
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ export const KimiRotatorPlugin: Plugin = async ({ client }) => {
73
+ openCodeClient = client;
74
+
75
+ accountManager = await getAccountManager();
76
+
77
+ const result = await accountManager.getNextAccount();
78
+
79
+ if (result) {
80
+ const kimiBaseUrl = 'https://api.kimi.com/coding/v1';
81
+ process.env.ANTHROPIC_BASE_URL = kimiBaseUrl;
82
+ process.env.ANTHROPIC_API_KEY = result.account.key;
83
+ }
84
+
85
+ originalFetch = globalThis.fetch;
86
+ globalThis.fetch = async (input: string | Request | URL, init?: RequestInit): Promise<Response> => {
87
+ const url = typeof input === 'string' ? input : input.toString();
88
+
89
+ if (url.includes('api.kimi.com')) {
90
+ const nextAccount = await accountManager!.getNextAccount();
91
+ if (nextAccount) {
92
+ currentAccountIndex = nextAccount.index;
93
+ const keyLabel = nextAccount.account.key.substring(0, 18) + '...';
94
+ const position = nextAccount.index + 1;
95
+ const allKeysNow = await accountManager!.listKeys();
96
+ const total = allKeysNow.length;
97
+
98
+ await showToast(`🔄 Using key: ${keyLabel} (${position}/${total})`, 'info');
99
+
100
+ const headers = new Headers(init?.headers);
101
+ headers.set('x-api-key', nextAccount.account.key);
102
+ headers.delete('Authorization');
103
+
104
+ try {
105
+ const response = await originalFetch!(input, { ...init, headers });
106
+
107
+ if (response.status === 429) {
108
+ const retryAfter = response.headers.get('retry-after');
109
+ const retryAfterMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : 60000;
110
+ await accountManager!.markAccountRateLimited(currentAccountIndex, retryAfterMs);
111
+ await showToast(`⚠️ Key ${position} rate limited`, 'warning');
112
+ } else if (response.ok) {
113
+ await accountManager!.markAccountSuccess(currentAccountIndex);
114
+ } else if (response.status >= 500) {
115
+ await accountManager!.markAccountFailure(currentAccountIndex);
116
+ }
117
+
118
+ return response;
119
+ } catch (error: unknown) {
120
+ await accountManager!.markAccountFailure(currentAccountIndex);
121
+ throw error;
122
+ }
123
+ }
124
+ }
125
+
126
+ return originalFetch!(input, init);
127
+ };
128
+
129
+ const showInitialToast = async () => {
130
+ await new Promise(resolve => setTimeout(resolve, 500));
131
+ await flushToastQueue();
132
+ await showToast("🎉 Kimi Rotator Plugin loaded!", 'info');
133
+ };
134
+
135
+ showInitialToast().catch(() => { });
136
+
137
+ const pluginReturn = {
138
+ auth: {
139
+ provider: 'kimi-rotator',
140
+ methods: [{
141
+ type: 'api' as const,
142
+ label: 'Kimi API Key',
143
+ }],
144
+ loader: async () => {
145
+ const auth = await getAuth();
146
+ if (!auth) {
147
+ throw new Error('No Kimi API keys configured. Run: opencode-kimi add-key');
148
+ }
149
+
150
+ const kimiBaseUrl = 'https://api.kimi.com/coding/v1';
151
+
152
+ return {
153
+ apiKey: auth.key,
154
+ baseURL: kimiBaseUrl,
155
+ async fetch(input: string | Request | URL, init?: RequestInit) {
156
+ return globalThis.fetch(input, init);
157
+ }
158
+ };
159
+ },
160
+ },
161
+ cleanup: () => {
162
+ if (originalFetch) {
163
+ globalThis.fetch = originalFetch;
164
+ originalFetch = null;
165
+ }
166
+ openCodeClient = null;
167
+ accountManager = null;
168
+ toastQueue = [];
169
+ },
170
+ };
171
+
172
+ return pluginReturn;
173
+ };
174
+
175
+ export default KimiRotatorPlugin;
176
+ export type { KimiAccount };