@zhafron/opencode-kiro-auth 1.5.0 → 1.5.1
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 +2 -2
- package/dist/core/account/account-selector.d.ts +4 -0
- package/dist/core/account/account-selector.js +20 -0
- package/dist/core/auth/idc-auth-method.js +25 -19
- package/dist/core/request/error-handler.d.ts +1 -0
- package/dist/core/request/error-handler.js +1 -1
- package/dist/core/request/request-handler.d.ts +2 -0
- package/dist/core/request/request-handler.js +20 -1
- package/dist/core/request/retry-strategy.d.ts +1 -3
- package/dist/core/request/retry-strategy.js +4 -8
- package/dist/plugin/cli.d.ts +1 -1
- package/dist/plugin/cli.js +42 -13
- package/dist/plugin/config/schema.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,8 +102,8 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
|
|
|
102
102
|
"default_region": "us-east-1",
|
|
103
103
|
"rate_limit_retry_delay_ms": 5000,
|
|
104
104
|
"rate_limit_max_retries": 3,
|
|
105
|
-
"max_request_iterations":
|
|
106
|
-
"request_timeout_ms":
|
|
105
|
+
"max_request_iterations": 20,
|
|
106
|
+
"request_timeout_ms": 120000,
|
|
107
107
|
"token_expiry_buffer_ms": 120000,
|
|
108
108
|
"usage_sync_max_retries": 3,
|
|
109
109
|
"auth_server_port_start": 19847,
|
|
@@ -12,10 +12,14 @@ export declare class AccountSelector {
|
|
|
12
12
|
private syncFromKiroCli;
|
|
13
13
|
private repository;
|
|
14
14
|
private triedEmptySync;
|
|
15
|
+
private circuitBreakerTrips;
|
|
16
|
+
private lastCircuitBreakerReset;
|
|
15
17
|
constructor(accountManager: AccountManager, config: AccountSelectorConfig, syncFromKiroCli: () => Promise<void>, repository: AccountRepository);
|
|
16
18
|
selectHealthyAccount(showToast: ToastFunction): Promise<ManagedAccount | null>;
|
|
17
19
|
private handleEmptyAccounts;
|
|
18
20
|
private formatUsageMessage;
|
|
21
|
+
private checkCircuitBreaker;
|
|
22
|
+
private resetCircuitBreaker;
|
|
19
23
|
private sleep;
|
|
20
24
|
}
|
|
21
25
|
export {};
|
|
@@ -4,6 +4,8 @@ export class AccountSelector {
|
|
|
4
4
|
syncFromKiroCli;
|
|
5
5
|
repository;
|
|
6
6
|
triedEmptySync = false;
|
|
7
|
+
circuitBreakerTrips = 0;
|
|
8
|
+
lastCircuitBreakerReset = Date.now();
|
|
7
9
|
constructor(accountManager, config, syncFromKiroCli, repository) {
|
|
8
10
|
this.accountManager = accountManager;
|
|
9
11
|
this.config = config;
|
|
@@ -11,6 +13,7 @@ export class AccountSelector {
|
|
|
11
13
|
this.repository = repository;
|
|
12
14
|
}
|
|
13
15
|
async selectHealthyAccount(showToast) {
|
|
16
|
+
this.checkCircuitBreaker();
|
|
14
17
|
let count = this.accountManager.getAccountCount();
|
|
15
18
|
if (count === 0 && this.config.auto_sync_kiro_cli && !this.triedEmptySync) {
|
|
16
19
|
this.triedEmptySync = true;
|
|
@@ -22,6 +25,7 @@ export class AccountSelector {
|
|
|
22
25
|
}
|
|
23
26
|
let acc = this.accountManager.getCurrentOrNext();
|
|
24
27
|
if (!acc) {
|
|
28
|
+
this.circuitBreakerTrips++;
|
|
25
29
|
const wait = this.accountManager.getMinWaitTime();
|
|
26
30
|
if (wait > 0 && wait < 30000) {
|
|
27
31
|
if (this.accountManager.shouldShowToast()) {
|
|
@@ -32,6 +36,7 @@ export class AccountSelector {
|
|
|
32
36
|
}
|
|
33
37
|
throw new Error('All accounts are unhealthy or rate-limited');
|
|
34
38
|
}
|
|
39
|
+
this.resetCircuitBreaker();
|
|
35
40
|
if (this.accountManager.shouldShowToast()) {
|
|
36
41
|
showToast(`Using ${acc.email} (${this.accountManager.getAccounts().indexOf(acc) + 1}/${count})`, 'info');
|
|
37
42
|
}
|
|
@@ -58,6 +63,21 @@ export class AccountSelector {
|
|
|
58
63
|
}
|
|
59
64
|
return `Usage (${email}): ${usedCount}`;
|
|
60
65
|
}
|
|
66
|
+
checkCircuitBreaker() {
|
|
67
|
+
if (Date.now() - this.lastCircuitBreakerReset > 60000) {
|
|
68
|
+
this.circuitBreakerTrips = 0;
|
|
69
|
+
this.lastCircuitBreakerReset = Date.now();
|
|
70
|
+
}
|
|
71
|
+
if (this.circuitBreakerTrips >= 10) {
|
|
72
|
+
throw new Error('Circuit breaker tripped: Too many consecutive failures selecting accounts');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
resetCircuitBreaker() {
|
|
76
|
+
if (this.circuitBreakerTrips > 0) {
|
|
77
|
+
this.circuitBreakerTrips = 0;
|
|
78
|
+
this.lastCircuitBreakerReset = Date.now();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
61
81
|
sleep(ms) {
|
|
62
82
|
return new Promise((r) => setTimeout(r, ms));
|
|
63
83
|
}
|
|
@@ -39,33 +39,39 @@ export class IdcAuthMethod {
|
|
|
39
39
|
async handleMultipleLogin(region, resolve) {
|
|
40
40
|
const accounts = [];
|
|
41
41
|
let startFresh = true;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
while (true) {
|
|
43
|
+
const existingAccounts = await this.repository.findAll();
|
|
44
|
+
const idcAccs = existingAccounts.filter((a) => a.authMethod === 'idc');
|
|
45
|
+
if (idcAccs.length === 0) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
45
48
|
const existingAccountsList = idcAccs.map((acc, idx) => ({
|
|
46
49
|
email: acc.email,
|
|
47
50
|
index: idx
|
|
48
51
|
}));
|
|
49
52
|
const mode = await promptLoginMode(existingAccountsList);
|
|
50
53
|
if (mode === 'delete') {
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const deleteIndices = await promptDeleteAccount(existingAccountsList);
|
|
55
|
+
if (deleteIndices !== null && deleteIndices.length > 0) {
|
|
56
|
+
for (const idx of deleteIndices) {
|
|
57
|
+
const accToDelete = idcAccs[idx];
|
|
58
|
+
if (accToDelete) {
|
|
59
|
+
await this.repository.delete(accToDelete.id);
|
|
60
|
+
console.log(`[Success] Deleted: ${accToDelete.email}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
console.log(`\n[Success] Deleted ${deleteIndices.length} account(s)\n`);
|
|
56
64
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
});
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (mode === 'add') {
|
|
68
|
+
startFresh = false;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (mode === 'fresh') {
|
|
72
|
+
startFresh = true;
|
|
73
|
+
break;
|
|
67
74
|
}
|
|
68
|
-
startFresh = mode === 'fresh';
|
|
69
75
|
}
|
|
70
76
|
while (true) {
|
|
71
77
|
try {
|
|
@@ -94,7 +94,7 @@ export class ErrorHandler {
|
|
|
94
94
|
}
|
|
95
95
|
async handleNetworkError(error, context, showToast) {
|
|
96
96
|
if (this.isNetworkError(error) && context.retry < this.config.rate_limit_max_retries) {
|
|
97
|
-
const d =
|
|
97
|
+
const d = this.config.rate_limit_retry_delay_ms * Math.pow(2, context.retry);
|
|
98
98
|
showToast(`Network error. Retrying in ${Math.ceil(d / 1000)}s...`, 'warning');
|
|
99
99
|
await this.sleep(d);
|
|
100
100
|
return {
|
|
@@ -28,7 +28,7 @@ export class RequestHandler {
|
|
|
28
28
|
this.errorHandler = new ErrorHandler(config, accountManager, repository);
|
|
29
29
|
this.responseHandler = new ResponseHandler();
|
|
30
30
|
this.usageTracker = new UsageTracker(config, accountManager, repository);
|
|
31
|
-
this.retryStrategy = new RetryStrategy(config
|
|
31
|
+
this.retryStrategy = new RetryStrategy(config);
|
|
32
32
|
}
|
|
33
33
|
async handle(input, init, showToast) {
|
|
34
34
|
const url = typeof input === 'string' ? input : input.url;
|
|
@@ -44,20 +44,29 @@ export class RequestHandler {
|
|
|
44
44
|
const budget = body.providerOptions?.thinkingConfig?.thinkingBudget || 20000;
|
|
45
45
|
let reductionFactor = 1.0;
|
|
46
46
|
let retry = 0;
|
|
47
|
+
let consecutiveNullAccounts = 0;
|
|
47
48
|
const retryContext = this.retryStrategy.createContext();
|
|
48
49
|
while (true) {
|
|
49
50
|
const check = this.retryStrategy.shouldContinue(retryContext);
|
|
50
51
|
if (!check.canContinue) {
|
|
51
52
|
throw new Error(check.error);
|
|
52
53
|
}
|
|
54
|
+
if (this.allAccountsPermanentlyUnhealthy()) {
|
|
55
|
+
throw new Error('All accounts are permanently unhealthy (quota exceeded or suspended)');
|
|
56
|
+
}
|
|
53
57
|
let acc = await this.accountSelector.selectHealthyAccount(showToast);
|
|
54
58
|
if (!acc) {
|
|
59
|
+
consecutiveNullAccounts++;
|
|
60
|
+
const backoffDelay = Math.min(1000 * Math.pow(2, consecutiveNullAccounts - 1), 10000);
|
|
61
|
+
await this.sleep(backoffDelay);
|
|
55
62
|
continue;
|
|
56
63
|
}
|
|
64
|
+
consecutiveNullAccounts = 0;
|
|
57
65
|
const auth = this.accountManager.toAuthDetails(acc);
|
|
58
66
|
const tokenResult = await this.tokenRefresher.refreshIfNeeded(acc, auth, showToast);
|
|
59
67
|
if (tokenResult.shouldContinue) {
|
|
60
68
|
acc = tokenResult.account;
|
|
69
|
+
await this.sleep(500);
|
|
61
70
|
continue;
|
|
62
71
|
}
|
|
63
72
|
const prep = this.prepareRequest(url, init?.body, model, auth, think, budget, reductionFactor);
|
|
@@ -177,4 +186,14 @@ export class RequestHandler {
|
|
|
177
186
|
}, rData, logger.getTimestamp());
|
|
178
187
|
}
|
|
179
188
|
}
|
|
189
|
+
allAccountsPermanentlyUnhealthy() {
|
|
190
|
+
const accounts = this.accountManager.getAccounts();
|
|
191
|
+
if (accounts.length === 0) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
return accounts.every((acc) => !acc.isHealthy && isPermanentError(acc.unhealthyReason));
|
|
195
|
+
}
|
|
196
|
+
sleep(ms) {
|
|
197
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
198
|
+
}
|
|
180
199
|
}
|
|
@@ -8,9 +8,7 @@ interface RetryContext {
|
|
|
8
8
|
}
|
|
9
9
|
export declare class RetryStrategy {
|
|
10
10
|
private config;
|
|
11
|
-
|
|
12
|
-
private timeoutMs;
|
|
13
|
-
constructor(config: RetryConfig, maxIterations: number, timeoutMs: number);
|
|
11
|
+
constructor(config: RetryConfig);
|
|
14
12
|
shouldContinue(context: RetryContext): {
|
|
15
13
|
canContinue: boolean;
|
|
16
14
|
error?: string;
|
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
export class RetryStrategy {
|
|
2
2
|
config;
|
|
3
|
-
|
|
4
|
-
timeoutMs;
|
|
5
|
-
constructor(config, maxIterations, timeoutMs) {
|
|
3
|
+
constructor(config) {
|
|
6
4
|
this.config = config;
|
|
7
|
-
this.maxIterations = maxIterations;
|
|
8
|
-
this.timeoutMs = timeoutMs;
|
|
9
5
|
}
|
|
10
6
|
shouldContinue(context) {
|
|
11
7
|
context.iterations++;
|
|
12
|
-
if (context.iterations > this.
|
|
8
|
+
if (context.iterations > this.config.max_request_iterations) {
|
|
13
9
|
return {
|
|
14
10
|
canContinue: false,
|
|
15
|
-
error: `Exceeded max iterations (${this.
|
|
11
|
+
error: `Exceeded max iterations (${this.config.max_request_iterations})`
|
|
16
12
|
};
|
|
17
13
|
}
|
|
18
|
-
if (Date.now() - context.startTime > this.
|
|
14
|
+
if (Date.now() - context.startTime > this.config.request_timeout_ms) {
|
|
19
15
|
return {
|
|
20
16
|
canContinue: false,
|
|
21
17
|
error: 'Request timeout'
|
package/dist/plugin/cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare function promptAddAnotherAccount(currentCount: number): Promise<boolean>;
|
|
2
|
-
export declare function promptDeleteAccount(accounts: ExistingAccountInfo[]): Promise<number | null>;
|
|
2
|
+
export declare function promptDeleteAccount(accounts: ExistingAccountInfo[]): Promise<number[] | null>;
|
|
3
3
|
export type LoginMode = 'add' | 'fresh' | 'delete';
|
|
4
4
|
export interface ExistingAccountInfo {
|
|
5
5
|
email?: string;
|
package/dist/plugin/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ export async function promptAddAnotherAccount(currentCount) {
|
|
|
14
14
|
export async function promptDeleteAccount(accounts) {
|
|
15
15
|
const rl = createInterface({ input, output });
|
|
16
16
|
try {
|
|
17
|
-
console.log(`\nSelect account to delete:`);
|
|
17
|
+
console.log(`\nSelect account(s) to delete:`);
|
|
18
18
|
for (const acc of accounts) {
|
|
19
19
|
const label = acc.email || `Account ${acc.index + 1}`;
|
|
20
20
|
console.log(` ${acc.index + 1}. ${label}`);
|
|
@@ -22,22 +22,51 @@ export async function promptDeleteAccount(accounts) {
|
|
|
22
22
|
console.log(` 0. Cancel`);
|
|
23
23
|
console.log('');
|
|
24
24
|
while (true) {
|
|
25
|
-
const answer = await rl.question('Enter account number: ');
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
25
|
+
const answer = await rl.question('Enter account number(s) (e.g., 1,2,3 or 1): ');
|
|
26
|
+
const trimmed = answer.trim();
|
|
27
|
+
if (trimmed === '0') {
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
36
|
-
|
|
30
|
+
const parts = trimmed.split(',').map((s) => s.trim());
|
|
31
|
+
const numbers = [];
|
|
32
|
+
let invalid = false;
|
|
33
|
+
for (const part of parts) {
|
|
34
|
+
const num = parseInt(part, 10);
|
|
35
|
+
if (isNaN(num) || num < 1 || num > accounts.length) {
|
|
36
|
+
invalid = true;
|
|
37
|
+
break;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
+
if (!numbers.includes(num)) {
|
|
40
|
+
numbers.push(num);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (invalid) {
|
|
44
|
+
console.log(`Please enter valid numbers between 1 and ${accounts.length}, separated by commas`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (numbers.length === 0) {
|
|
48
|
+
console.log(`Please enter at least one account number`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const indices = numbers.map((n) => n - 1);
|
|
52
|
+
const selectedAccounts = indices
|
|
53
|
+
.map((i) => accounts[i])
|
|
54
|
+
.filter((acc) => acc !== undefined);
|
|
55
|
+
if (selectedAccounts.length === 0) {
|
|
56
|
+
console.log(`No valid accounts selected`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
console.log(`\nYou are about to delete ${selectedAccounts.length} account(s):`);
|
|
60
|
+
for (const acc of selectedAccounts) {
|
|
61
|
+
const label = acc.email || `Account ${acc.index + 1}`;
|
|
62
|
+
console.log(` - ${label}`);
|
|
63
|
+
}
|
|
64
|
+
const confirm = await rl.question(`\nConfirm deletion? (y/n): `);
|
|
65
|
+
const normalized = confirm.trim().toLowerCase();
|
|
66
|
+
if (normalized === 'y' || normalized === 'yes') {
|
|
67
|
+
return selectedAccounts.map((acc) => acc.index);
|
|
39
68
|
}
|
|
40
|
-
|
|
69
|
+
return null;
|
|
41
70
|
}
|
|
42
71
|
}
|
|
43
72
|
finally {
|
|
@@ -7,8 +7,8 @@ export const KiroConfigSchema = z.object({
|
|
|
7
7
|
default_region: RegionSchema.default('us-east-1'),
|
|
8
8
|
rate_limit_retry_delay_ms: z.number().min(1000).max(60000).default(5000),
|
|
9
9
|
rate_limit_max_retries: z.number().min(0).max(10).default(3),
|
|
10
|
-
max_request_iterations: z.number().min(
|
|
11
|
-
request_timeout_ms: z.number().min(
|
|
10
|
+
max_request_iterations: z.number().min(5).max(1000).default(20),
|
|
11
|
+
request_timeout_ms: z.number().min(30000).max(600000).default(120000),
|
|
12
12
|
token_expiry_buffer_ms: z.number().min(30000).max(300000).default(120000),
|
|
13
13
|
usage_sync_max_retries: z.number().min(0).max(5).default(3),
|
|
14
14
|
auth_server_port_start: z.number().min(1024).max(65535).default(19847),
|
|
@@ -22,8 +22,8 @@ export const DEFAULT_CONFIG = {
|
|
|
22
22
|
default_region: 'us-east-1',
|
|
23
23
|
rate_limit_retry_delay_ms: 5000,
|
|
24
24
|
rate_limit_max_retries: 3,
|
|
25
|
-
max_request_iterations:
|
|
26
|
-
request_timeout_ms:
|
|
25
|
+
max_request_iterations: 20,
|
|
26
|
+
request_timeout_ms: 120000,
|
|
27
27
|
token_expiry_buffer_ms: 120000,
|
|
28
28
|
usage_sync_max_retries: 3,
|
|
29
29
|
auth_server_port_start: 19847,
|