@sunerpy/opencode-kiro-auth 0.1.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.
Files changed (113) hide show
  1. package/LICENSE +678 -0
  2. package/README.md +270 -0
  3. package/dist/constants.d.ts +28 -0
  4. package/dist/constants.js +108 -0
  5. package/dist/core/account/account-selector.d.ts +25 -0
  6. package/dist/core/account/account-selector.js +80 -0
  7. package/dist/core/account/usage-tracker.d.ts +20 -0
  8. package/dist/core/account/usage-tracker.js +56 -0
  9. package/dist/core/auth/auth-handler.d.ts +18 -0
  10. package/dist/core/auth/auth-handler.js +236 -0
  11. package/dist/core/auth/idc-auth-method.d.ts +9 -0
  12. package/dist/core/auth/idc-auth-method.js +169 -0
  13. package/dist/core/auth/token-refresher.d.ts +23 -0
  14. package/dist/core/auth/token-refresher.js +85 -0
  15. package/dist/core/index.d.ts +9 -0
  16. package/dist/core/index.js +9 -0
  17. package/dist/core/request/error-handler.d.ts +32 -0
  18. package/dist/core/request/error-handler.js +159 -0
  19. package/dist/core/request/request-handler.d.ts +35 -0
  20. package/dist/core/request/request-handler.js +296 -0
  21. package/dist/core/request/response-handler.d.ts +8 -0
  22. package/dist/core/request/response-handler.js +137 -0
  23. package/dist/core/request/retry-strategy.d.ts +18 -0
  24. package/dist/core/request/retry-strategy.js +28 -0
  25. package/dist/index.d.ts +94 -0
  26. package/dist/index.js +2 -0
  27. package/dist/infrastructure/database/account-cache.d.ts +14 -0
  28. package/dist/infrastructure/database/account-cache.js +44 -0
  29. package/dist/infrastructure/database/account-repository.d.ts +12 -0
  30. package/dist/infrastructure/database/account-repository.js +66 -0
  31. package/dist/infrastructure/index.d.ts +7 -0
  32. package/dist/infrastructure/index.js +7 -0
  33. package/dist/infrastructure/transformers/event-stream-parser.d.ts +7 -0
  34. package/dist/infrastructure/transformers/event-stream-parser.js +115 -0
  35. package/dist/infrastructure/transformers/history-builder.d.ts +16 -0
  36. package/dist/infrastructure/transformers/history-builder.js +226 -0
  37. package/dist/infrastructure/transformers/message-transformer.d.ts +5 -0
  38. package/dist/infrastructure/transformers/message-transformer.js +99 -0
  39. package/dist/infrastructure/transformers/tool-call-parser.d.ts +4 -0
  40. package/dist/infrastructure/transformers/tool-call-parser.js +45 -0
  41. package/dist/infrastructure/transformers/tool-transformer.d.ts +2 -0
  42. package/dist/infrastructure/transformers/tool-transformer.js +19 -0
  43. package/dist/kiro/auth.d.ts +4 -0
  44. package/dist/kiro/auth.js +25 -0
  45. package/dist/kiro/oauth-idc.d.ts +25 -0
  46. package/dist/kiro/oauth-idc.js +167 -0
  47. package/dist/plugin/accounts.d.ts +29 -0
  48. package/dist/plugin/accounts.js +266 -0
  49. package/dist/plugin/auth-bootstrap.d.ts +9 -0
  50. package/dist/plugin/auth-bootstrap.js +69 -0
  51. package/dist/plugin/auth-page.d.ts +4 -0
  52. package/dist/plugin/auth-page.js +721 -0
  53. package/dist/plugin/config/index.d.ts +3 -0
  54. package/dist/plugin/config/index.js +2 -0
  55. package/dist/plugin/config/loader.d.ts +6 -0
  56. package/dist/plugin/config/loader.js +129 -0
  57. package/dist/plugin/config/schema.d.ts +88 -0
  58. package/dist/plugin/config/schema.js +94 -0
  59. package/dist/plugin/effort.d.ts +46 -0
  60. package/dist/plugin/effort.js +113 -0
  61. package/dist/plugin/errors.d.ts +17 -0
  62. package/dist/plugin/errors.js +34 -0
  63. package/dist/plugin/health.d.ts +1 -0
  64. package/dist/plugin/health.js +13 -0
  65. package/dist/plugin/image-handler.d.ts +18 -0
  66. package/dist/plugin/image-handler.js +82 -0
  67. package/dist/plugin/logger.d.ts +8 -0
  68. package/dist/plugin/logger.js +84 -0
  69. package/dist/plugin/models.d.ts +2 -0
  70. package/dist/plugin/models.js +11 -0
  71. package/dist/plugin/request.d.ts +9 -0
  72. package/dist/plugin/request.js +287 -0
  73. package/dist/plugin/response.d.ts +3 -0
  74. package/dist/plugin/response.js +97 -0
  75. package/dist/plugin/sdk-client.d.ts +4 -0
  76. package/dist/plugin/sdk-client.js +55 -0
  77. package/dist/plugin/storage/locked-operations.d.ts +5 -0
  78. package/dist/plugin/storage/locked-operations.js +103 -0
  79. package/dist/plugin/storage/migrations.d.ts +4 -0
  80. package/dist/plugin/storage/migrations.js +155 -0
  81. package/dist/plugin/storage/sqlite.d.ts +18 -0
  82. package/dist/plugin/storage/sqlite.js +166 -0
  83. package/dist/plugin/streaming/index.d.ts +2 -0
  84. package/dist/plugin/streaming/index.js +2 -0
  85. package/dist/plugin/streaming/openai-converter.d.ts +2 -0
  86. package/dist/plugin/streaming/openai-converter.js +81 -0
  87. package/dist/plugin/streaming/sdk-stream-transformer.d.ts +1 -0
  88. package/dist/plugin/streaming/sdk-stream-transformer.js +274 -0
  89. package/dist/plugin/streaming/stream-parser.d.ts +5 -0
  90. package/dist/plugin/streaming/stream-parser.js +136 -0
  91. package/dist/plugin/streaming/stream-state.d.ts +5 -0
  92. package/dist/plugin/streaming/stream-state.js +59 -0
  93. package/dist/plugin/streaming/stream-transformer.d.ts +1 -0
  94. package/dist/plugin/streaming/stream-transformer.js +295 -0
  95. package/dist/plugin/streaming/types.d.ts +25 -0
  96. package/dist/plugin/streaming/types.js +2 -0
  97. package/dist/plugin/sync/kiro-cli-parser.d.ts +8 -0
  98. package/dist/plugin/sync/kiro-cli-parser.js +72 -0
  99. package/dist/plugin/sync/kiro-cli-profile.d.ts +1 -0
  100. package/dist/plugin/sync/kiro-cli-profile.js +30 -0
  101. package/dist/plugin/sync/kiro-cli.d.ts +2 -0
  102. package/dist/plugin/sync/kiro-cli.js +212 -0
  103. package/dist/plugin/sync/stale-accounts.d.ts +9 -0
  104. package/dist/plugin/sync/stale-accounts.js +31 -0
  105. package/dist/plugin/token.d.ts +2 -0
  106. package/dist/plugin/token.js +78 -0
  107. package/dist/plugin/types.d.ts +125 -0
  108. package/dist/plugin/types.js +0 -0
  109. package/dist/plugin/usage.d.ts +3 -0
  110. package/dist/plugin/usage.js +79 -0
  111. package/dist/plugin.d.ts +174 -0
  112. package/dist/plugin.js +170 -0
  113. package/package.json +74 -0
@@ -0,0 +1,159 @@
1
+ export class ErrorHandler {
2
+ config;
3
+ accountManager;
4
+ repository;
5
+ forceRefresh;
6
+ constructor(config, accountManager, repository, forceRefresh) {
7
+ this.config = config;
8
+ this.accountManager = accountManager;
9
+ this.repository = repository;
10
+ this.forceRefresh = forceRefresh;
11
+ }
12
+ async handle(error, response, account, context, showToast) {
13
+ const readBody = async () => {
14
+ try {
15
+ const body = JSON.parse(await response.clone().text());
16
+ return body.message || body.Message || body.__type || JSON.stringify(body);
17
+ }
18
+ catch {
19
+ return '';
20
+ }
21
+ };
22
+ if (response.status === 400) {
23
+ const reason = await readBody();
24
+ showToast(`400: ${reason || 'unknown'}`, 'error');
25
+ return { shouldRetry: false };
26
+ }
27
+ if (response.status === 401 && context.retry < this.config.rate_limit_max_retries) {
28
+ const reason = await readBody();
29
+ showToast(`401: ${reason || 'Unauthorized'}. Retrying...`, 'warning');
30
+ return {
31
+ shouldRetry: true,
32
+ newContext: { ...context, retry: context.retry + 1 }
33
+ };
34
+ }
35
+ if (response.status === 500) {
36
+ account.failCount = (account.failCount || 0) + 1;
37
+ let errorMessage = 'Internal Server Error';
38
+ try {
39
+ const errorBody = await response.text();
40
+ const errorData = JSON.parse(errorBody);
41
+ if (errorData.message) {
42
+ errorMessage = errorData.message;
43
+ }
44
+ else if (errorData.Message) {
45
+ errorMessage = errorData.Message;
46
+ }
47
+ }
48
+ catch (e) { }
49
+ if (account.failCount < 5) {
50
+ const delay = 1000 * Math.pow(2, account.failCount - 1);
51
+ showToast(`500: ${errorMessage}. Retrying in ${Math.ceil(delay / 1000)}s...`, 'warning');
52
+ await this.sleep(delay);
53
+ return { shouldRetry: true };
54
+ }
55
+ else {
56
+ this.accountManager.markUnhealthy(account, `Server Error (500) after 5 attempts: ${errorMessage}`);
57
+ await this.repository.batchSave(this.accountManager.getAccounts());
58
+ showToast(`500: ${errorMessage}. Marking account as unhealthy and switching...`, 'warning');
59
+ return { shouldRetry: true, switchAccount: true };
60
+ }
61
+ }
62
+ if (response.status === 429) {
63
+ const w = parseInt(response.headers.get('retry-after') || '60') * 1000;
64
+ this.accountManager.markRateLimited(account, w);
65
+ await this.repository.batchSave(this.accountManager.getAccounts());
66
+ const count = this.accountManager.getAccountCount();
67
+ if (count > 1) {
68
+ return { shouldRetry: true, switchAccount: true };
69
+ }
70
+ showToast(`429: Rate limited. Waiting ${Math.ceil(w / 1000)}s...`, 'warning');
71
+ await this.sleep(w);
72
+ return { shouldRetry: true };
73
+ }
74
+ if (response.status === 402 || response.status === 403) {
75
+ let errorReason = response.status === 402 ? 'Quota' : 'Forbidden';
76
+ let isPermanent = false;
77
+ const errorBody = await response.text();
78
+ const errorData = (() => {
79
+ try {
80
+ return JSON.parse(errorBody);
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ })();
86
+ if (errorData?.message) {
87
+ errorReason = errorData.message;
88
+ }
89
+ if (errorData?.reason === 'INVALID_MODEL_ID') {
90
+ throw new Error(`Invalid model: ${errorData.message}`);
91
+ }
92
+ if (errorData?.reason === 'TEMPORARILY_SUSPENDED') {
93
+ errorReason = 'Account Suspended';
94
+ isPermanent = true;
95
+ }
96
+ const isInvalidBearer = errorReason.includes('bearer token included in the request is invalid') ||
97
+ errorReason.includes('The bearer token included in the request is invalid');
98
+ if (response.status === 403 &&
99
+ isInvalidBearer &&
100
+ !context.bearerRefreshAttempted &&
101
+ this.forceRefresh) {
102
+ const refreshed = await this.forceRefresh(account, showToast);
103
+ if (refreshed) {
104
+ showToast('403: Stale token detected. Refreshed and retrying...', 'warning');
105
+ return {
106
+ shouldRetry: true,
107
+ newContext: { ...context, bearerRefreshAttempted: true }
108
+ };
109
+ }
110
+ }
111
+ if (isInvalidBearer) {
112
+ isPermanent = true;
113
+ }
114
+ if (isPermanent) {
115
+ account.failCount = 10;
116
+ }
117
+ if (this.accountManager.getAccountCount() > 1) {
118
+ showToast(`${response.status}: ${errorReason}. Switching account...`, 'warning');
119
+ this.accountManager.markUnhealthy(account, errorReason);
120
+ await this.repository.batchSave(this.accountManager.getAccounts());
121
+ return { shouldRetry: true, switchAccount: true };
122
+ }
123
+ if (response.status === 403 &&
124
+ !isPermanent &&
125
+ context.retry < this.config.rate_limit_max_retries) {
126
+ const delay = this.config.rate_limit_retry_delay_ms * Math.pow(2, context.retry);
127
+ showToast(`403: ${errorReason}. Retrying in ${Math.ceil(delay / 1000)}s...`, 'warning');
128
+ await this.sleep(delay);
129
+ return {
130
+ shouldRetry: true,
131
+ newContext: { ...context, retry: context.retry + 1 }
132
+ };
133
+ }
134
+ showToast(`${response.status}: ${errorReason}`, 'error');
135
+ return { shouldRetry: false };
136
+ }
137
+ const reason = await readBody();
138
+ showToast(`${response.status}: ${reason || response.statusText}`, 'error');
139
+ return { shouldRetry: false };
140
+ }
141
+ async handleNetworkError(error, context, showToast) {
142
+ if (this.isNetworkError(error) && context.retry < this.config.rate_limit_max_retries) {
143
+ const d = this.config.rate_limit_retry_delay_ms * Math.pow(2, context.retry);
144
+ showToast(`Network error. Retrying in ${Math.ceil(d / 1000)}s...`, 'warning');
145
+ await this.sleep(d);
146
+ return {
147
+ shouldRetry: true,
148
+ newContext: { ...context, retry: context.retry + 1 }
149
+ };
150
+ }
151
+ return { shouldRetry: false };
152
+ }
153
+ isNetworkError(e) {
154
+ return (e instanceof Error && /econnreset|etimedout|enotfound|network|fetch failed/i.test(e.message));
155
+ }
156
+ sleep(ms) {
157
+ return new Promise((r) => setTimeout(r, ms));
158
+ }
159
+ }
@@ -0,0 +1,35 @@
1
+ import type { AccountRepository } from '../../infrastructure/database/account-repository.js';
2
+ import type { AccountManager } from '../../plugin/accounts.js';
3
+ import type { KiroConfig } from '../../plugin/config/index.js';
4
+ type ToastFunction = (message: string, variant: 'info' | 'warning' | 'success' | 'error') => void;
5
+ export declare class RequestHandler {
6
+ private accountManager;
7
+ private config;
8
+ private repository;
9
+ private client?;
10
+ private accountSelector;
11
+ private tokenRefresher;
12
+ private errorHandler;
13
+ private responseHandler;
14
+ private usageTracker;
15
+ private retryStrategy;
16
+ private reauthInFlight;
17
+ private lastFailedReauthAt;
18
+ private static kiroRequestQueue;
19
+ constructor(accountManager: AccountManager, config: KiroConfig, repository: AccountRepository, client?: any | undefined);
20
+ handle(input: any, init: any, showToast: ToastFunction): Promise<Response>;
21
+ private enqueueKiroRequest;
22
+ private handleKiroRequest;
23
+ private extractModel;
24
+ private prepareSdkRequest;
25
+ private handleSuccessfulRequest;
26
+ private logSdkRequest;
27
+ private logSdkResponse;
28
+ private logSdkError;
29
+ private triggerReauth;
30
+ private performReauth;
31
+ private hasUsableAccount;
32
+ private allAccountsPermanentlyUnhealthy;
33
+ private sleep;
34
+ }
35
+ export {};
@@ -0,0 +1,296 @@
1
+ import { GenerateAssistantResponseCommand } from '@aws/codewhisperer-streaming-client';
2
+ import { isPermanentError } from '../../plugin/health.js';
3
+ import * as logger from '../../plugin/logger.js';
4
+ import { transformToSdkRequest } from '../../plugin/request.js';
5
+ import { createSdkClient } from '../../plugin/sdk-client.js';
6
+ import { syncFromKiroCli } from '../../plugin/sync/kiro-cli.js';
7
+ import { AccountSelector } from '../account/account-selector.js';
8
+ import { UsageTracker } from '../account/usage-tracker.js';
9
+ import { TokenRefresher } from '../auth/token-refresher.js';
10
+ import { ErrorHandler } from './error-handler.js';
11
+ import { ResponseHandler } from './response-handler.js';
12
+ import { RetryStrategy } from './retry-strategy.js';
13
+ const KIRO_API_PATTERN = /^(https?:\/\/)?q\.[a-z0-9-]+\.amazonaws\.com/;
14
+ const REAUTH_FAILURE_COOLDOWN_MS = 60000;
15
+ export class RequestHandler {
16
+ accountManager;
17
+ config;
18
+ repository;
19
+ client;
20
+ accountSelector;
21
+ tokenRefresher;
22
+ errorHandler;
23
+ responseHandler;
24
+ usageTracker;
25
+ retryStrategy;
26
+ reauthInFlight = null;
27
+ lastFailedReauthAt = 0;
28
+ static kiroRequestQueue = Promise.resolve();
29
+ constructor(accountManager, config, repository, client) {
30
+ this.accountManager = accountManager;
31
+ this.config = config;
32
+ this.repository = repository;
33
+ this.client = client;
34
+ this.accountSelector = new AccountSelector(accountManager, config, syncFromKiroCli, repository);
35
+ this.tokenRefresher = new TokenRefresher(config, accountManager, syncFromKiroCli, repository);
36
+ this.errorHandler = new ErrorHandler(config, accountManager, repository, (acc, toast) => this.tokenRefresher.forceRefresh(acc, toast));
37
+ this.responseHandler = new ResponseHandler();
38
+ this.usageTracker = new UsageTracker(config, accountManager, repository);
39
+ this.retryStrategy = new RetryStrategy(config);
40
+ }
41
+ async handle(input, init, showToast) {
42
+ const url = typeof input === 'string' ? input : input.url;
43
+ if (!KIRO_API_PATTERN.test(url)) {
44
+ return fetch(input, init);
45
+ }
46
+ return this.enqueueKiroRequest(() => this.handleKiroRequest(url, init, showToast));
47
+ }
48
+ async enqueueKiroRequest(run) {
49
+ const previous = RequestHandler.kiroRequestQueue;
50
+ let release;
51
+ RequestHandler.kiroRequestQueue = new Promise((resolve) => {
52
+ release = resolve;
53
+ });
54
+ await previous.catch(() => { });
55
+ try {
56
+ return await run();
57
+ }
58
+ finally {
59
+ release();
60
+ }
61
+ }
62
+ async handleKiroRequest(url, init, showToast) {
63
+ const body = init?.body ? JSON.parse(init.body) : {};
64
+ const model = this.extractModel(url) || body.model || 'claude-sonnet-4-5';
65
+ const think = model.endsWith('-thinking') || !!body.providerOptions?.thinkingConfig || !!body.thinkingConfig;
66
+ const budget = body.providerOptions?.thinkingConfig?.thinkingBudget ||
67
+ body.thinkingConfig?.thinkingBudget ||
68
+ body.thinkingConfig?.budget_tokens ||
69
+ 20000;
70
+ let handlerContext = { retry: 0 };
71
+ let consecutiveNullAccounts = 0;
72
+ const retryContext = this.retryStrategy.createContext();
73
+ while (true) {
74
+ const check = this.retryStrategy.shouldContinue(retryContext);
75
+ if (!check.canContinue) {
76
+ throw new Error(check.error);
77
+ }
78
+ if (this.allAccountsPermanentlyUnhealthy()) {
79
+ const reauthed = await this.triggerReauth(showToast);
80
+ if (!reauthed) {
81
+ throw new Error('All accounts are permanently unhealthy. Please re-authenticate.');
82
+ }
83
+ continue;
84
+ }
85
+ let acc = await this.accountSelector.selectHealthyAccount(showToast).catch(async (e) => {
86
+ if (e instanceof Error && e.message.includes('reauth required')) {
87
+ const reauthed = await this.triggerReauth(showToast);
88
+ if (!reauthed)
89
+ throw new Error('All accounts are unhealthy or rate-limited. Please re-authenticate.');
90
+ return null;
91
+ }
92
+ throw e;
93
+ });
94
+ if (!acc) {
95
+ consecutiveNullAccounts++;
96
+ const backoffDelay = Math.min(1000 * Math.pow(2, consecutiveNullAccounts - 1), 10000);
97
+ await this.sleep(backoffDelay);
98
+ continue;
99
+ }
100
+ consecutiveNullAccounts = 0;
101
+ const auth = this.accountManager.toAuthDetails(acc);
102
+ const tokenResult = await this.tokenRefresher.refreshIfNeeded(acc, auth, showToast);
103
+ if (tokenResult.shouldContinue) {
104
+ acc = tokenResult.account;
105
+ await this.sleep(500);
106
+ continue;
107
+ }
108
+ const sdkPrep = this.prepareSdkRequest(init?.body, model, auth, think, budget, showToast);
109
+ const apiTimestamp = this.config.enable_log_api_request ? logger.getTimestamp() : null;
110
+ if (apiTimestamp) {
111
+ this.logSdkRequest(sdkPrep, acc, apiTimestamp);
112
+ }
113
+ try {
114
+ const client = createSdkClient(auth, sdkPrep.region, sdkPrep.effort);
115
+ const command = new GenerateAssistantResponseCommand({
116
+ conversationState: sdkPrep.conversationState,
117
+ profileArn: sdkPrep.profileArn
118
+ });
119
+ const sdkResponse = await client.send(command);
120
+ if (apiTimestamp) {
121
+ this.logSdkResponse(sdkPrep, apiTimestamp);
122
+ }
123
+ this.handleSuccessfulRequest(acc);
124
+ this.usageTracker.syncUsage(acc, auth);
125
+ return await this.responseHandler.handleSdkSuccess(sdkResponse, model, sdkPrep.conversationId, sdkPrep.streaming);
126
+ }
127
+ catch (e) {
128
+ const httpStatus = e?.$metadata?.httpStatusCode;
129
+ if (httpStatus) {
130
+ if (apiTimestamp) {
131
+ this.logSdkError(sdkPrep, e, acc, apiTimestamp);
132
+ }
133
+ const mockResponse = new Response(JSON.stringify({ message: e.message, __type: e.name }), {
134
+ status: httpStatus,
135
+ statusText: e.name || 'Error',
136
+ headers: { 'Content-Type': 'application/json' }
137
+ });
138
+ const errorResult = await this.errorHandler.handle(e, mockResponse, acc, handlerContext, showToast);
139
+ if (errorResult.shouldRetry) {
140
+ if (errorResult.newContext) {
141
+ handlerContext = errorResult.newContext;
142
+ }
143
+ if (errorResult.switchAccount) {
144
+ continue;
145
+ }
146
+ continue;
147
+ }
148
+ throw new Error(`Kiro Error: ${httpStatus}`);
149
+ }
150
+ const networkResult = await this.errorHandler.handleNetworkError(e, handlerContext, showToast);
151
+ if (networkResult.shouldRetry) {
152
+ if (networkResult.newContext) {
153
+ handlerContext = { ...handlerContext, ...networkResult.newContext };
154
+ }
155
+ continue;
156
+ }
157
+ throw e;
158
+ }
159
+ }
160
+ }
161
+ extractModel(url) {
162
+ return url.match(/models\/([^/:]+)/)?.[1] || null;
163
+ }
164
+ prepareSdkRequest(body, model, auth, think, budget, showToast) {
165
+ return transformToSdkRequest(body, model, auth, think, budget, showToast, {
166
+ effort: this.config.effort,
167
+ autoEffortMapping: this.config.auto_effort_mapping
168
+ });
169
+ }
170
+ handleSuccessfulRequest(acc) {
171
+ if (acc.failCount && acc.failCount > 0) {
172
+ if (!isPermanentError(acc.unhealthyReason)) {
173
+ acc.failCount = 0;
174
+ acc.isHealthy = true;
175
+ delete acc.unhealthyReason;
176
+ delete acc.recoveryTime;
177
+ this.repository.save(acc).catch(() => { });
178
+ }
179
+ }
180
+ }
181
+ logSdkRequest(prep, acc, timestamp) {
182
+ logger.logApiRequest({
183
+ url: `https://q.${prep.region}.amazonaws.com/generateAssistantResponse`,
184
+ method: 'POST',
185
+ headers: { 'x-amzn-kiro-agent-mode': 'vibe' },
186
+ body: {
187
+ conversationState: {
188
+ chatTriggerType: prep.conversationState.chatTriggerType,
189
+ conversationId: prep.conversationState.conversationId,
190
+ historyLength: prep.conversationState.history?.length || 0,
191
+ currentMessage: prep.conversationState.currentMessage
192
+ },
193
+ profileArn: prep.profileArn
194
+ },
195
+ conversationId: prep.conversationId,
196
+ model: prep.effectiveModel,
197
+ email: acc.email
198
+ }, timestamp);
199
+ }
200
+ logSdkResponse(prep, timestamp) {
201
+ logger.logApiResponse({
202
+ status: 200,
203
+ statusText: 'OK',
204
+ headers: {},
205
+ conversationId: prep.conversationId,
206
+ model: prep.effectiveModel
207
+ }, timestamp);
208
+ }
209
+ logSdkError(prep, error, acc, apiTimestamp) {
210
+ const status = error?.$metadata?.httpStatusCode || 0;
211
+ const rData = {
212
+ status,
213
+ statusText: error?.name || 'Error',
214
+ headers: {},
215
+ error: `Kiro Error: ${status} - ${error?.message || 'Unknown'}`,
216
+ conversationId: prep.conversationId,
217
+ model: prep.effectiveModel
218
+ };
219
+ if (!this.config.enable_log_api_request) {
220
+ logger.logApiError({
221
+ url: `https://q.${prep.region}.amazonaws.com/generateAssistantResponse`,
222
+ method: 'POST',
223
+ headers: {},
224
+ body: null,
225
+ conversationId: prep.conversationId,
226
+ model: prep.effectiveModel,
227
+ email: acc.email
228
+ }, rData, logger.getTimestamp());
229
+ }
230
+ else {
231
+ logger.logApiResponse(rData, apiTimestamp);
232
+ }
233
+ }
234
+ async triggerReauth(showToast) {
235
+ if (!this.client)
236
+ return false;
237
+ const cooldownRemaining = REAUTH_FAILURE_COOLDOWN_MS - (Date.now() - this.lastFailedReauthAt);
238
+ if (cooldownRemaining > 0) {
239
+ showToast('Recent re-authentication failed. Please complete authentication manually.', 'error');
240
+ return false;
241
+ }
242
+ if (this.reauthInFlight) {
243
+ return this.reauthInFlight;
244
+ }
245
+ this.reauthInFlight = this.performReauth(showToast);
246
+ const success = await this.reauthInFlight.finally(() => {
247
+ this.reauthInFlight = null;
248
+ });
249
+ if (!success)
250
+ this.lastFailedReauthAt = Date.now();
251
+ return success;
252
+ }
253
+ async performReauth(showToast) {
254
+ try {
255
+ showToast('Session expired. Re-authenticating...', 'warning');
256
+ await this.client.provider.oauth.authorize({
257
+ path: { id: 'kiro-auth' },
258
+ body: { method: 0 }
259
+ });
260
+ await this.client.provider.oauth.callback({
261
+ path: { id: 'kiro-auth' },
262
+ body: { method: 0 }
263
+ });
264
+ this.repository.invalidateCache();
265
+ const accounts = await this.repository.findAll();
266
+ for (const acc of accounts) {
267
+ this.accountManager.addAccount(acc);
268
+ }
269
+ if (!this.hasUsableAccount(accounts)) {
270
+ logger.warn('Re-auth completed but no usable Kiro account was found');
271
+ showToast('Re-authentication completed but no usable Kiro account was found.', 'error');
272
+ return false;
273
+ }
274
+ showToast('Re-authentication successful.', 'success');
275
+ return true;
276
+ }
277
+ catch (e) {
278
+ logger.error('Re-auth failed', e instanceof Error ? e : new Error(String(e)));
279
+ return false;
280
+ }
281
+ }
282
+ hasUsableAccount(accounts) {
283
+ const now = Date.now();
284
+ return accounts.some((acc) => acc.isHealthy && acc.expiresAt > now && !isPermanentError(acc.unhealthyReason));
285
+ }
286
+ allAccountsPermanentlyUnhealthy() {
287
+ const accounts = this.accountManager.getAccounts();
288
+ if (accounts.length === 0) {
289
+ return false;
290
+ }
291
+ return accounts.every((acc) => !acc.isHealthy && isPermanentError(acc.unhealthyReason));
292
+ }
293
+ sleep(ms) {
294
+ return new Promise((resolve) => setTimeout(resolve, ms));
295
+ }
296
+ }
@@ -0,0 +1,8 @@
1
+ export declare class ResponseHandler {
2
+ handleSuccess(response: Response, model: string, conversationId: string, streaming: boolean): Promise<Response>;
3
+ handleSdkSuccess(sdkResponse: any, model: string, conversationId: string, streaming: boolean): Promise<Response>;
4
+ private handleStreaming;
5
+ private handleSdkStreaming;
6
+ private handleNonStreaming;
7
+ private handleSdkNonStreaming;
8
+ }
@@ -0,0 +1,137 @@
1
+ import { parseEventStream } from '../../plugin/response.js';
2
+ import { transformKiroStream } from '../../plugin/streaming/index.js';
3
+ import { transformSdkStream } from '../../plugin/streaming/sdk-stream-transformer.js';
4
+ export class ResponseHandler {
5
+ async handleSuccess(response, model, conversationId, streaming) {
6
+ if (streaming) {
7
+ return this.handleStreaming(response, model, conversationId);
8
+ }
9
+ return this.handleNonStreaming(response, model, conversationId);
10
+ }
11
+ async handleSdkSuccess(sdkResponse, model, conversationId, streaming) {
12
+ if (streaming) {
13
+ return this.handleSdkStreaming(sdkResponse, model, conversationId);
14
+ }
15
+ return this.handleSdkNonStreaming(sdkResponse, model, conversationId);
16
+ }
17
+ async handleStreaming(response, model, conversationId) {
18
+ const s = transformKiroStream(response, model, conversationId);
19
+ return new Response(new ReadableStream({
20
+ async start(c) {
21
+ try {
22
+ for await (const e of s) {
23
+ c.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(e)}\n\n`));
24
+ }
25
+ c.close();
26
+ }
27
+ catch (err) {
28
+ c.error(err);
29
+ }
30
+ }
31
+ }), { headers: { 'Content-Type': 'text/event-stream' } });
32
+ }
33
+ async handleSdkStreaming(sdkResponse, model, conversationId) {
34
+ const s = transformSdkStream(sdkResponse, model, conversationId);
35
+ return new Response(new ReadableStream({
36
+ async start(c) {
37
+ try {
38
+ for await (const e of s) {
39
+ c.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(e)}\n\n`));
40
+ }
41
+ c.close();
42
+ }
43
+ catch (err) {
44
+ c.error(err);
45
+ }
46
+ }
47
+ }), { headers: { 'Content-Type': 'text/event-stream' } });
48
+ }
49
+ async handleNonStreaming(response, model, conversationId) {
50
+ const text = await response.text();
51
+ const p = parseEventStream(text, model);
52
+ const oai = {
53
+ id: conversationId,
54
+ object: 'chat.completion',
55
+ created: Math.floor(Date.now() / 1000),
56
+ model,
57
+ choices: [
58
+ {
59
+ index: 0,
60
+ message: { role: 'assistant', content: p.content },
61
+ finish_reason: p.stopReason === 'tool_use' ? 'tool_calls' : 'stop'
62
+ }
63
+ ],
64
+ usage: {
65
+ prompt_tokens: p.inputTokens || 0,
66
+ completion_tokens: p.outputTokens || 0,
67
+ total_tokens: (p.inputTokens || 0) + (p.outputTokens || 0)
68
+ }
69
+ };
70
+ if (p.toolCalls.length > 0) {
71
+ oai.choices[0].message.tool_calls = p.toolCalls.map((tc) => ({
72
+ id: tc.toolUseId,
73
+ type: 'function',
74
+ function: {
75
+ name: tc.name,
76
+ arguments: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input)
77
+ }
78
+ }));
79
+ }
80
+ return new Response(JSON.stringify(oai), {
81
+ headers: { 'Content-Type': 'application/json' }
82
+ });
83
+ }
84
+ async handleSdkNonStreaming(sdkResponse, model, conversationId) {
85
+ // For non-streaming SDK responses, collect all events
86
+ let content = '';
87
+ const toolCalls = [];
88
+ let inputTokens = 0;
89
+ let outputTokens = 0;
90
+ const eventStream = sdkResponse.generateAssistantResponseResponse;
91
+ if (eventStream) {
92
+ for await (const event of eventStream) {
93
+ if (event.assistantResponseEvent?.content) {
94
+ content += event.assistantResponseEvent.content;
95
+ }
96
+ if (event.toolUseEvent) {
97
+ toolCalls.push(event.toolUseEvent);
98
+ }
99
+ if (event.metadataEvent?.tokenUsage) {
100
+ inputTokens = event.metadataEvent.tokenUsage.inputTokens || 0;
101
+ outputTokens = event.metadataEvent.tokenUsage.outputTokens || 0;
102
+ }
103
+ }
104
+ }
105
+ const oai = {
106
+ id: conversationId,
107
+ object: 'chat.completion',
108
+ created: Math.floor(Date.now() / 1000),
109
+ model,
110
+ choices: [
111
+ {
112
+ index: 0,
113
+ message: { role: 'assistant', content },
114
+ finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop'
115
+ }
116
+ ],
117
+ usage: {
118
+ prompt_tokens: inputTokens,
119
+ completion_tokens: outputTokens,
120
+ total_tokens: inputTokens + outputTokens
121
+ }
122
+ };
123
+ if (toolCalls.length > 0) {
124
+ oai.choices[0].message.tool_calls = toolCalls.map((tc) => ({
125
+ id: tc.toolUseId,
126
+ type: 'function',
127
+ function: {
128
+ name: tc.name,
129
+ arguments: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input)
130
+ }
131
+ }));
132
+ }
133
+ return new Response(JSON.stringify(oai), {
134
+ headers: { 'Content-Type': 'application/json' }
135
+ });
136
+ }
137
+ }
@@ -0,0 +1,18 @@
1
+ interface RetryConfig {
2
+ max_request_iterations: number;
3
+ request_timeout_ms: number;
4
+ }
5
+ interface RetryContext {
6
+ iterations: number;
7
+ startTime: number;
8
+ }
9
+ export declare class RetryStrategy {
10
+ private config;
11
+ constructor(config: RetryConfig);
12
+ shouldContinue(context: RetryContext): {
13
+ canContinue: boolean;
14
+ error?: string;
15
+ };
16
+ createContext(): RetryContext;
17
+ }
18
+ export {};
@@ -0,0 +1,28 @@
1
+ export class RetryStrategy {
2
+ config;
3
+ constructor(config) {
4
+ this.config = config;
5
+ }
6
+ shouldContinue(context) {
7
+ context.iterations++;
8
+ if (context.iterations > this.config.max_request_iterations) {
9
+ return {
10
+ canContinue: false,
11
+ error: `Exceeded max iterations (${this.config.max_request_iterations})`
12
+ };
13
+ }
14
+ if (Date.now() - context.startTime > this.config.request_timeout_ms) {
15
+ return {
16
+ canContinue: false,
17
+ error: 'Request timeout'
18
+ };
19
+ }
20
+ return { canContinue: true };
21
+ }
22
+ createContext() {
23
+ return {
24
+ iterations: 0,
25
+ startTime: Date.now()
26
+ };
27
+ }
28
+ }