@sunerpy/opencode-kiro-auth 0.5.2 → 0.6.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.
@@ -6,8 +6,18 @@ export declare class AccountManager {
6
6
  private strategy;
7
7
  private lastToastTime;
8
8
  private lastUsageToastTime;
9
- constructor(accounts: ManagedAccount[], strategy?: AccountSelectionStrategy);
10
- static loadFromDisk(strategy?: AccountSelectionStrategy): Promise<AccountManager>;
9
+ private rrCursor;
10
+ private stickyId?;
11
+ private quotaAvoidanceEnabled;
12
+ private quotaReserveThreshold;
13
+ constructor(accounts: ManagedAccount[], strategy?: AccountSelectionStrategy, opts?: {
14
+ quotaAvoidanceEnabled?: boolean;
15
+ quotaReserveThreshold?: number;
16
+ });
17
+ static loadFromDisk(strategy?: AccountSelectionStrategy, opts?: {
18
+ quotaAvoidanceEnabled?: boolean;
19
+ quotaReserveThreshold?: number;
20
+ }): Promise<AccountManager>;
11
21
  getAccountCount(): number;
12
22
  getAccounts(): ManagedAccount[];
13
23
  shouldShowToast(debounce?: number): boolean;
@@ -15,12 +15,18 @@ export class AccountManager {
15
15
  strategy;
16
16
  lastToastTime = 0;
17
17
  lastUsageToastTime = 0;
18
- constructor(accounts, strategy = 'sticky') {
18
+ rrCursor = 0;
19
+ stickyId;
20
+ quotaAvoidanceEnabled;
21
+ quotaReserveThreshold;
22
+ constructor(accounts, strategy = 'sticky', opts) {
19
23
  this.accounts = accounts;
20
24
  this.cursor = 0;
21
25
  this.strategy = strategy;
26
+ this.quotaAvoidanceEnabled = opts?.quotaAvoidanceEnabled ?? true;
27
+ this.quotaReserveThreshold = opts?.quotaReserveThreshold ?? 0.95;
22
28
  }
23
- static async loadFromDisk(strategy) {
29
+ static async loadFromDisk(strategy, opts) {
24
30
  const rows = kiroDb.getAccounts();
25
31
  const accounts = rows.map((r) => ({
26
32
  id: r.id,
@@ -44,7 +50,7 @@ export class AccountManager {
44
50
  usedCount: r.used_count,
45
51
  limitCount: r.limit_count
46
52
  }));
47
- return new AccountManager(accounts, strategy || 'sticky');
53
+ return new AccountManager(accounts, strategy || 'sticky', opts);
48
54
  }
49
55
  getAccountCount() {
50
56
  return this.accounts.length;
@@ -86,17 +92,29 @@ export class AccountManager {
86
92
  }
87
93
  return !(a.rateLimitResetTime && now < a.rateLimitResetTime);
88
94
  });
95
+ let candidatePool = available;
96
+ if (this.accounts.length > 1 && this.quotaAvoidanceEnabled) {
97
+ const ratio = (a) => a.limitCount && a.limitCount > 0 ? (a.usedCount || 0) / a.limitCount : 0;
98
+ const ample = available.filter((a) => ratio(a) < this.quotaReserveThreshold);
99
+ // used>=limit stays in nearFull (soft/drainable, NOT hard-excluded): the
100
+ // real 402 is the authoritative exhaustion signal and already
101
+ // hard-switches accounts in error-handler.
102
+ const nearFull = available.filter((a) => ratio(a) >= this.quotaReserveThreshold);
103
+ candidatePool = ample.length > 0 ? ample : nearFull;
104
+ }
89
105
  let selected;
90
- if (available.length > 0) {
106
+ if (candidatePool.length > 0) {
91
107
  if (this.strategy === 'sticky') {
92
- selected = available.find((_, i) => i === this.cursor) || available[0];
108
+ selected = candidatePool.find((a) => a.id === this.stickyId) || candidatePool[0];
109
+ if (selected)
110
+ this.stickyId = selected.id;
93
111
  }
94
112
  else if (this.strategy === 'round-robin') {
95
- selected = available[this.cursor % available.length];
96
- this.cursor = (this.cursor + 1) % available.length;
113
+ selected = candidatePool[this.rrCursor % candidatePool.length];
114
+ this.rrCursor++;
97
115
  }
98
116
  else if (this.strategy === 'lowest-usage') {
99
- selected = [...available].sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
117
+ selected = [...candidatePool].sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
100
118
  }
101
119
  }
102
120
  if (!selected) {
@@ -113,7 +131,6 @@ export class AccountManager {
113
131
  if (selected) {
114
132
  selected.lastUsed = now;
115
133
  selected.usedCount = (selected.usedCount || 0) + 1;
116
- this.cursor = this.accounts.indexOf(selected);
117
134
  return selected;
118
135
  }
119
136
  return null;
@@ -90,6 +90,8 @@ function applyEnvOverrides(config) {
90
90
  account_selection_strategy: env.KIRO_ACCOUNT_SELECTION_STRATEGY
91
91
  ? AccountSelectionStrategySchema.catch('lowest-usage').parse(env.KIRO_ACCOUNT_SELECTION_STRATEGY)
92
92
  : config.account_selection_strategy,
93
+ quota_avoidance_enabled: parseBooleanEnv(env.KIRO_QUOTA_AVOIDANCE_ENABLED, config.quota_avoidance_enabled),
94
+ quota_reserve_threshold: parseNumberEnv(env.KIRO_QUOTA_RESERVE_THRESHOLD, config.quota_reserve_threshold),
93
95
  default_region: env.KIRO_DEFAULT_REGION
94
96
  ? RegionSchema.catch('us-east-1').parse(env.KIRO_DEFAULT_REGION)
95
97
  : config.default_region,
@@ -19,6 +19,19 @@ export declare const KiroConfigSchema: z.ZodObject<{
19
19
  idc_region: z.ZodOptional<z.ZodEnum<["us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-2", "ap-southeast-3", "ap-southeast-5", "ap-southeast-4", "ap-south-1", "ap-southeast-6", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-east-2", "ap-southeast-7", "ap-northeast-1", "ca-central-1", "ca-west-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-south-2", "eu-north-1", "eu-central-2", "il-central-1", "mx-central-1", "me-south-1", "me-central-1", "sa-east-1"]>>;
20
20
  idc_profile_arn: z.ZodOptional<z.ZodString>;
21
21
  account_selection_strategy: z.ZodDefault<z.ZodEnum<["sticky", "round-robin", "lowest-usage"]>>;
22
+ /**
23
+ * Softly avoid accounts whose usage ratio is at/above
24
+ * `quota_reserve_threshold` when other accounts still have room. When ALL
25
+ * healthy accounts are near-full they are drained anyway (the real 402 in
26
+ * error-handler is the authoritative hard-switch). Only affects
27
+ * multi-account selection; single-account behavior is unchanged.
28
+ */
29
+ quota_avoidance_enabled: z.ZodDefault<z.ZodBoolean>;
30
+ /**
31
+ * Usage ratio (used/limit) at/above which an account is considered
32
+ * near-full and softly avoided. Default 0.95 (95%).
33
+ */
34
+ quota_reserve_threshold: z.ZodDefault<z.ZodNumber>;
22
35
  default_region: z.ZodDefault<z.ZodEnum<["us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-2", "ap-southeast-3", "ap-southeast-5", "ap-southeast-4", "ap-south-1", "ap-southeast-6", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-east-2", "ap-southeast-7", "ap-northeast-1", "ca-central-1", "ca-west-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-south-2", "eu-north-1", "eu-central-2", "il-central-1", "mx-central-1", "me-south-1", "me-central-1", "sa-east-1"]>>;
23
36
  rate_limit_retry_delay_ms: z.ZodDefault<z.ZodNumber>;
24
37
  rate_limit_max_retries: z.ZodDefault<z.ZodNumber>;
@@ -52,6 +65,8 @@ export declare const KiroConfigSchema: z.ZodObject<{
52
65
  auto_effort_mapping: z.ZodDefault<z.ZodBoolean>;
53
66
  }, "strip", z.ZodTypeAny, {
54
67
  account_selection_strategy: "sticky" | "round-robin" | "lowest-usage";
68
+ quota_avoidance_enabled: boolean;
69
+ quota_reserve_threshold: number;
55
70
  default_region: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1";
56
71
  rate_limit_retry_delay_ms: number;
57
72
  rate_limit_max_retries: number;
@@ -77,6 +92,8 @@ export declare const KiroConfigSchema: z.ZodObject<{
77
92
  idc_region?: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1" | undefined;
78
93
  idc_profile_arn?: string | undefined;
79
94
  account_selection_strategy?: "sticky" | "round-robin" | "lowest-usage" | undefined;
95
+ quota_avoidance_enabled?: boolean | undefined;
96
+ quota_reserve_threshold?: number | undefined;
80
97
  default_region?: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1" | undefined;
81
98
  rate_limit_retry_delay_ms?: number | undefined;
82
99
  rate_limit_max_retries?: number | undefined;
@@ -51,6 +51,19 @@ export const KiroConfigSchema = z.object({
51
51
  idc_region: RegionSchema.optional(),
52
52
  idc_profile_arn: z.string().optional(),
53
53
  account_selection_strategy: AccountSelectionStrategySchema.default('lowest-usage'),
54
+ /**
55
+ * Softly avoid accounts whose usage ratio is at/above
56
+ * `quota_reserve_threshold` when other accounts still have room. When ALL
57
+ * healthy accounts are near-full they are drained anyway (the real 402 in
58
+ * error-handler is the authoritative hard-switch). Only affects
59
+ * multi-account selection; single-account behavior is unchanged.
60
+ */
61
+ quota_avoidance_enabled: z.boolean().default(true),
62
+ /**
63
+ * Usage ratio (used/limit) at/above which an account is considered
64
+ * near-full and softly avoided. Default 0.95 (95%).
65
+ */
66
+ quota_reserve_threshold: z.number().min(0).max(1).default(0.95),
54
67
  default_region: RegionSchema.default('us-east-1'),
55
68
  rate_limit_retry_delay_ms: z.number().min(1000).max(60000).default(5000),
56
69
  rate_limit_max_retries: z.number().min(0).max(10).default(3),
@@ -85,6 +98,8 @@ export const KiroConfigSchema = z.object({
85
98
  });
86
99
  export const DEFAULT_CONFIG = {
87
100
  account_selection_strategy: 'lowest-usage',
101
+ quota_avoidance_enabled: true,
102
+ quota_reserve_threshold: 0.95,
88
103
  default_region: 'us-east-1',
89
104
  rate_limit_retry_delay_ms: 5000,
90
105
  rate_limit_max_retries: 3,
package/dist/plugin.js CHANGED
@@ -15,7 +15,10 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
15
15
  const cache = new AccountCache(60000);
16
16
  const repository = new AccountRepository(cache);
17
17
  const authHandler = new AuthHandler(config, repository);
18
- const accountManager = await AccountManager.loadFromDisk(config.account_selection_strategy);
18
+ const accountManager = await AccountManager.loadFromDisk(config.account_selection_strategy, {
19
+ quotaAvoidanceEnabled: config.quota_avoidance_enabled,
20
+ quotaReserveThreshold: config.quota_reserve_threshold
21
+ });
19
22
  authHandler.setAccountManager(accountManager);
20
23
  const requestHandler = new RequestHandler(accountManager, config, repository, client);
21
24
  // Compute the base URL once so both the config hook and auth loader use the same value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunerpy/opencode-kiro-auth",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",