@sunerpy/opencode-kiro-auth 0.13.1 → 0.13.3
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/dist/core/account/account-selector.d.ts +2 -1
- package/dist/core/account/account-selector.js +20 -4
- package/dist/core/account/usage-tracker.d.ts +1 -1
- package/dist/core/account/usage-tracker.js +18 -4
- package/dist/core/request/error-handler.d.ts +2 -2
- package/dist/core/request/error-handler.js +24 -11
- package/dist/core/request/request-handler.d.ts +4 -0
- package/dist/core/request/request-handler.js +217 -105
- package/dist/core/request/response-handler.d.ts +11 -2
- package/dist/core/request/response-handler.js +239 -58
- package/dist/core/request/stream-error.d.ts +20 -0
- package/dist/core/request/stream-error.js +31 -0
- package/dist/plugin/accounts.d.ts +4 -1
- package/dist/plugin/accounts.js +10 -3
- package/package.json +1 -1
|
@@ -15,7 +15,8 @@ export declare class AccountSelector {
|
|
|
15
15
|
private circuitBreakerTrips;
|
|
16
16
|
private lastCircuitBreakerReset;
|
|
17
17
|
constructor(accountManager: AccountManager, config: AccountSelectorConfig, syncFromKiroCli: () => Promise<void>, repository: AccountRepository);
|
|
18
|
-
selectHealthyAccount(showToast: ToastFunction): Promise<ManagedAccount | null>;
|
|
18
|
+
selectHealthyAccount(showToast: ToastFunction, signal?: AbortSignal): Promise<ManagedAccount | null>;
|
|
19
|
+
selectAlternativeAccount(excludedIds: ReadonlySet<string>): Promise<ManagedAccount | null>;
|
|
19
20
|
private handleEmptyAccounts;
|
|
20
21
|
private formatUsageMessage;
|
|
21
22
|
private checkCircuitBreaker;
|
|
@@ -12,7 +12,7 @@ export class AccountSelector {
|
|
|
12
12
|
this.syncFromKiroCli = syncFromKiroCli;
|
|
13
13
|
this.repository = repository;
|
|
14
14
|
}
|
|
15
|
-
async selectHealthyAccount(showToast) {
|
|
15
|
+
async selectHealthyAccount(showToast, signal) {
|
|
16
16
|
this.checkCircuitBreaker();
|
|
17
17
|
let count = this.accountManager.getAccountCount();
|
|
18
18
|
if (count === 0 && this.config.auto_sync_kiro_cli && !this.triedEmptySync) {
|
|
@@ -34,7 +34,7 @@ export class AccountSelector {
|
|
|
34
34
|
if (this.accountManager.shouldShowToast()) {
|
|
35
35
|
showToast(`All accounts rate-limited. Waiting ${Math.ceil(wait / 1000)}s...`, 'warning');
|
|
36
36
|
}
|
|
37
|
-
await this.sleep(wait);
|
|
37
|
+
await this.sleep(wait, signal);
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
40
|
throw new Error('All accounts are unhealthy or rate-limited: reauth required');
|
|
@@ -47,6 +47,9 @@ export class AccountSelector {
|
|
|
47
47
|
}
|
|
48
48
|
return acc;
|
|
49
49
|
}
|
|
50
|
+
async selectAlternativeAccount(excludedIds) {
|
|
51
|
+
return this.accountManager.getCurrentOrNext({ excludedIds, recoverUnhealthy: false });
|
|
52
|
+
}
|
|
50
53
|
async handleEmptyAccounts() {
|
|
51
54
|
await this.syncFromKiroCli();
|
|
52
55
|
this.repository.invalidateCache();
|
|
@@ -77,7 +80,20 @@ export class AccountSelector {
|
|
|
77
80
|
this.lastCircuitBreakerReset = Date.now();
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
|
-
sleep(ms) {
|
|
81
|
-
|
|
83
|
+
sleep(ms, signal) {
|
|
84
|
+
if (signal?.aborted)
|
|
85
|
+
return Promise.reject(signal.reason);
|
|
86
|
+
let onAbort;
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const timer = setTimeout(resolve, ms);
|
|
89
|
+
onAbort = () => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
reject(signal?.reason);
|
|
92
|
+
};
|
|
93
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
94
|
+
}).finally(() => {
|
|
95
|
+
if (onAbort)
|
|
96
|
+
signal?.removeEventListener('abort', onAbort);
|
|
97
|
+
});
|
|
82
98
|
}
|
|
83
99
|
}
|
|
@@ -13,7 +13,7 @@ export declare class UsageTracker {
|
|
|
13
13
|
private lastSyncTime;
|
|
14
14
|
private readonly cooldownMs;
|
|
15
15
|
constructor(config: UsageTrackerConfig, accountManager: AccountManager, repository: AccountRepository);
|
|
16
|
-
syncUsage(account: ManagedAccount, auth: KiroAuthDetails): Promise<void>;
|
|
16
|
+
syncUsage(account: ManagedAccount, auth: KiroAuthDetails, isValid?: () => boolean): Promise<void>;
|
|
17
17
|
private syncWithRetry;
|
|
18
18
|
private sleep;
|
|
19
19
|
}
|
|
@@ -12,30 +12,40 @@ export class UsageTracker {
|
|
|
12
12
|
this.repository = repository;
|
|
13
13
|
this.cooldownMs = config.usage_sync_cooldown_ms ?? 60000;
|
|
14
14
|
}
|
|
15
|
-
async syncUsage(account, auth) {
|
|
15
|
+
async syncUsage(account, auth, isValid = () => true) {
|
|
16
16
|
if (!this.config.usage_tracking_enabled)
|
|
17
17
|
return;
|
|
18
|
+
if (!isValid())
|
|
19
|
+
return;
|
|
18
20
|
const last = this.lastSyncTime.get(account.id) ?? 0;
|
|
19
21
|
if (Date.now() - last < this.cooldownMs)
|
|
20
22
|
return;
|
|
21
23
|
this.lastSyncTime.set(account.id, Date.now());
|
|
22
|
-
this.syncWithRetry(account, auth, 0).catch((e) => {
|
|
24
|
+
this.syncWithRetry(account, auth, 0, isValid).catch((e) => {
|
|
23
25
|
logger.warn('Usage sync failed after all retries', {
|
|
24
26
|
accountId: account.id,
|
|
25
27
|
error: e instanceof Error ? e.message : String(e)
|
|
26
28
|
});
|
|
27
29
|
});
|
|
28
30
|
}
|
|
29
|
-
async syncWithRetry(account, auth, attempt) {
|
|
31
|
+
async syncWithRetry(account, auth, attempt, isValid) {
|
|
30
32
|
try {
|
|
31
33
|
const u = await fetchUsageLimits(auth);
|
|
34
|
+
if (!isValid())
|
|
35
|
+
return;
|
|
32
36
|
updateAccountQuota(account, u, this.accountManager);
|
|
37
|
+
if (!isValid())
|
|
38
|
+
return;
|
|
33
39
|
await this.repository.batchSave(this.accountManager.getAccounts());
|
|
34
40
|
}
|
|
35
41
|
catch (e) {
|
|
42
|
+
if (!isValid())
|
|
43
|
+
return;
|
|
36
44
|
if (attempt < this.config.usage_sync_max_retries) {
|
|
37
45
|
await this.sleep(1000 * Math.pow(2, attempt));
|
|
38
|
-
|
|
46
|
+
if (!isValid())
|
|
47
|
+
return;
|
|
48
|
+
return this.syncWithRetry(account, auth, attempt + 1, isValid);
|
|
39
49
|
}
|
|
40
50
|
if (e.message?.includes('FEATURE_NOT_SUPPORTED')) {
|
|
41
51
|
// Some IDC profiles don't support getUsageLimits; don't penalize the account.
|
|
@@ -44,7 +54,11 @@ export class UsageTracker {
|
|
|
44
54
|
if (e.message?.includes('403') ||
|
|
45
55
|
e.message?.includes('invalid') ||
|
|
46
56
|
e.message?.includes('bearer token')) {
|
|
57
|
+
if (!isValid())
|
|
58
|
+
return;
|
|
47
59
|
this.accountManager.markUnhealthy(account, e.message);
|
|
60
|
+
if (!isValid())
|
|
61
|
+
return;
|
|
48
62
|
this.repository.save(account).catch(() => { });
|
|
49
63
|
}
|
|
50
64
|
throw e;
|
|
@@ -19,14 +19,14 @@ export declare class ErrorHandler {
|
|
|
19
19
|
private repository;
|
|
20
20
|
private forceRefresh?;
|
|
21
21
|
constructor(config: ErrorHandlerConfig, accountManager: AccountManager, repository: AccountRepository, forceRefresh?: ForceRefreshFn | undefined);
|
|
22
|
-
handle(error: any, response: Response, account: ManagedAccount, context: RequestContext, showToast: ToastFunction): Promise<{
|
|
22
|
+
handle(error: any, response: Response, account: ManagedAccount, context: RequestContext, showToast: ToastFunction, signal?: AbortSignal): Promise<{
|
|
23
23
|
shouldRetry: boolean;
|
|
24
24
|
newContext?: RequestContext;
|
|
25
25
|
switchAccount?: boolean;
|
|
26
26
|
}>;
|
|
27
27
|
private markDeadAndSwitchOrFail;
|
|
28
28
|
private transientForbidden;
|
|
29
|
-
handleNetworkError(error: any, context: RequestContext, showToast: ToastFunction): Promise<{
|
|
29
|
+
handleNetworkError(error: any, context: RequestContext, showToast: ToastFunction, signal?: AbortSignal): Promise<{
|
|
30
30
|
shouldRetry: boolean;
|
|
31
31
|
newContext?: RequestContext;
|
|
32
32
|
}>;
|
|
@@ -17,7 +17,7 @@ export class ErrorHandler {
|
|
|
17
17
|
this.repository = repository;
|
|
18
18
|
this.forceRefresh = forceRefresh;
|
|
19
19
|
}
|
|
20
|
-
async handle(error, response, account, context, showToast) {
|
|
20
|
+
async handle(error, response, account, context, showToast, signal) {
|
|
21
21
|
const readBody = async () => {
|
|
22
22
|
try {
|
|
23
23
|
const body = JSON.parse(await response.clone().text());
|
|
@@ -57,7 +57,7 @@ export class ErrorHandler {
|
|
|
57
57
|
if (account.failCount < 5) {
|
|
58
58
|
const delay = 1000 * Math.pow(2, account.failCount - 1);
|
|
59
59
|
showToast(`500: ${errorMessage}. Retrying in ${Math.ceil(delay / 1000)}s...`, 'warning');
|
|
60
|
-
await this.sleep(delay);
|
|
60
|
+
await this.sleep(delay, signal);
|
|
61
61
|
return { shouldRetry: true };
|
|
62
62
|
}
|
|
63
63
|
else {
|
|
@@ -76,7 +76,7 @@ export class ErrorHandler {
|
|
|
76
76
|
return { shouldRetry: true, switchAccount: true };
|
|
77
77
|
}
|
|
78
78
|
showToast(`429: Rate limited. Waiting ${Math.ceil(w / 1000)}s...`, 'warning');
|
|
79
|
-
await this.sleep(w);
|
|
79
|
+
await this.sleep(w, signal);
|
|
80
80
|
return { shouldRetry: true };
|
|
81
81
|
}
|
|
82
82
|
if (response.status === 402 || response.status === 403) {
|
|
@@ -118,7 +118,7 @@ export class ErrorHandler {
|
|
|
118
118
|
if (result.dead) {
|
|
119
119
|
return this.markDeadAndSwitchOrFail(account, errorReason, response.status, context, nextForced, showToast);
|
|
120
120
|
}
|
|
121
|
-
return this.transientForbidden(errorReason, response.status, { ...context, forcedRefreshAccountIds: nextForced }, showToast);
|
|
121
|
+
return this.transientForbidden(errorReason, response.status, { ...context, forcedRefreshAccountIds: nextForced }, showToast, signal);
|
|
122
122
|
}
|
|
123
123
|
return this.markDeadAndSwitchOrFail(account, errorReason, response.status, context, forced, showToast);
|
|
124
124
|
}
|
|
@@ -134,7 +134,7 @@ export class ErrorHandler {
|
|
|
134
134
|
if (response.status === 403 &&
|
|
135
135
|
!isPermanent &&
|
|
136
136
|
context.retry < this.config.rate_limit_max_retries) {
|
|
137
|
-
return this.transientForbidden(errorReason, response.status, context, showToast);
|
|
137
|
+
return this.transientForbidden(errorReason, response.status, context, showToast, signal);
|
|
138
138
|
}
|
|
139
139
|
showToast(`${response.status}: ${errorReason}`, 'error');
|
|
140
140
|
return { shouldRetry: false };
|
|
@@ -158,24 +158,24 @@ export class ErrorHandler {
|
|
|
158
158
|
showToast(`${status}: ${errorReason}. Re-login required.`, 'error');
|
|
159
159
|
return { shouldRetry: false };
|
|
160
160
|
}
|
|
161
|
-
async transientForbidden(errorReason, status, context, showToast) {
|
|
161
|
+
async transientForbidden(errorReason, status, context, showToast, signal) {
|
|
162
162
|
if (context.retry >= this.config.rate_limit_max_retries) {
|
|
163
163
|
showToast(`${status}: ${errorReason}`, 'error');
|
|
164
164
|
return { shouldRetry: false };
|
|
165
165
|
}
|
|
166
166
|
const delay = this.config.rate_limit_retry_delay_ms * Math.pow(2, context.retry);
|
|
167
167
|
showToast(`${status}: ${errorReason}. Retrying in ${Math.ceil(delay / 1000)}s...`, 'warning');
|
|
168
|
-
await this.sleep(delay);
|
|
168
|
+
await this.sleep(delay, signal);
|
|
169
169
|
return {
|
|
170
170
|
shouldRetry: true,
|
|
171
171
|
newContext: { ...context, retry: context.retry + 1 }
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
|
-
async handleNetworkError(error, context, showToast) {
|
|
174
|
+
async handleNetworkError(error, context, showToast, signal) {
|
|
175
175
|
if (this.isNetworkError(error) && context.retry < this.config.rate_limit_max_retries) {
|
|
176
176
|
const d = this.config.rate_limit_retry_delay_ms * Math.pow(2, context.retry);
|
|
177
177
|
showToast(`Network error. Retrying in ${Math.ceil(d / 1000)}s...`, 'warning');
|
|
178
|
-
await this.sleep(d);
|
|
178
|
+
await this.sleep(d, signal);
|
|
179
179
|
return {
|
|
180
180
|
shouldRetry: true,
|
|
181
181
|
newContext: { ...context, retry: context.retry + 1 }
|
|
@@ -186,7 +186,20 @@ export class ErrorHandler {
|
|
|
186
186
|
isNetworkError(e) {
|
|
187
187
|
return (e instanceof Error && /econnreset|etimedout|enotfound|network|fetch failed/i.test(e.message));
|
|
188
188
|
}
|
|
189
|
-
sleep(ms) {
|
|
190
|
-
|
|
189
|
+
sleep(ms, signal) {
|
|
190
|
+
if (signal?.aborted)
|
|
191
|
+
return Promise.reject(signal.reason);
|
|
192
|
+
let onAbort;
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const timer = setTimeout(resolve, ms);
|
|
195
|
+
onAbort = () => {
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
reject(signal?.reason);
|
|
198
|
+
};
|
|
199
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
200
|
+
}).finally(() => {
|
|
201
|
+
if (onAbort)
|
|
202
|
+
signal?.removeEventListener('abort', onAbort);
|
|
203
|
+
});
|
|
191
204
|
}
|
|
192
205
|
}
|
|
@@ -16,6 +16,8 @@ export declare class RequestHandler {
|
|
|
16
16
|
private retryStrategy;
|
|
17
17
|
private reauthInFlight;
|
|
18
18
|
private lastFailedReauthAt;
|
|
19
|
+
private accountAttemptEpochs;
|
|
20
|
+
private streamRetryRandom;
|
|
19
21
|
private static kiroRequestQueue;
|
|
20
22
|
constructor(accountManager: AccountManager, config: KiroConfig, repository: AccountRepository, client?: any | undefined);
|
|
21
23
|
get sharedTokenRefresher(): TokenRefresher;
|
|
@@ -38,6 +40,8 @@ export declare class RequestHandler {
|
|
|
38
40
|
private performReauth;
|
|
39
41
|
private hasUsableAccount;
|
|
40
42
|
private allAccountsPermanentlyUnhealthy;
|
|
43
|
+
private nextAccountAttemptEpoch;
|
|
44
|
+
private getStreamRetryDelay;
|
|
41
45
|
private sleep;
|
|
42
46
|
}
|
|
43
47
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GenerateAssistantResponseCommand } from '@aws/codewhisperer-streaming-client';
|
|
2
|
+
import { clearTimeout as clearDeadlineTimeout, setTimeout as setDeadlineTimeout } from 'node:timers';
|
|
2
3
|
import { isPermanentError } from '../../plugin/health.js';
|
|
3
4
|
import * as logger from '../../plugin/logger.js';
|
|
4
5
|
import { transformToSdkRequest } from '../../plugin/request.js';
|
|
@@ -10,6 +11,7 @@ import { TokenRefresher } from '../auth/token-refresher.js';
|
|
|
10
11
|
import { ErrorHandler, isKiroContextOverflowBody } from './error-handler.js';
|
|
11
12
|
import { ResponseHandler } from './response-handler.js';
|
|
12
13
|
import { RetryStrategy } from './retry-strategy.js';
|
|
14
|
+
import { SdkEventStreamIterationError, UpstreamUnexpectedError } from './stream-error.js';
|
|
13
15
|
const KIRO_API_PATTERN = /^(https?:\/\/)?q\.[a-z0-9-]+\.amazonaws\.com/;
|
|
14
16
|
const REAUTH_FAILURE_COOLDOWN_MS = 60000;
|
|
15
17
|
export class RequestHandler {
|
|
@@ -25,6 +27,8 @@ export class RequestHandler {
|
|
|
25
27
|
retryStrategy;
|
|
26
28
|
reauthInFlight = null;
|
|
27
29
|
lastFailedReauthAt = 0;
|
|
30
|
+
accountAttemptEpochs = new Map();
|
|
31
|
+
streamRetryRandom = Math.random;
|
|
28
32
|
static kiroRequestQueue = Promise.resolve();
|
|
29
33
|
constructor(accountManager, config, repository, client) {
|
|
30
34
|
this.accountManager = accountManager;
|
|
@@ -63,6 +67,33 @@ export class RequestHandler {
|
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
async handleKiroRequest(url, init, showToast) {
|
|
70
|
+
const requestController = new AbortController();
|
|
71
|
+
const inboundSignal = init?.signal;
|
|
72
|
+
const abortFromInbound = () => requestController.abort(inboundSignal?.reason);
|
|
73
|
+
if (inboundSignal?.aborted)
|
|
74
|
+
abortFromInbound();
|
|
75
|
+
else
|
|
76
|
+
inboundSignal?.addEventListener('abort', abortFromInbound, { once: true });
|
|
77
|
+
let timeout;
|
|
78
|
+
const refreshRequestDeadline = () => {
|
|
79
|
+
if (requestController.signal.aborted)
|
|
80
|
+
return;
|
|
81
|
+
if (timeout)
|
|
82
|
+
clearDeadlineTimeout(timeout);
|
|
83
|
+
timeout = setDeadlineTimeout(() => requestController.abort(new DOMException('Kiro request timed out', 'TimeoutError')), this.config.request_timeout_ms);
|
|
84
|
+
};
|
|
85
|
+
refreshRequestDeadline();
|
|
86
|
+
let requestCleanupDone = false;
|
|
87
|
+
let responseOwnsLifecycle = false;
|
|
88
|
+
const cleanupRequest = () => {
|
|
89
|
+
if (requestCleanupDone)
|
|
90
|
+
return;
|
|
91
|
+
requestCleanupDone = true;
|
|
92
|
+
if (timeout)
|
|
93
|
+
clearDeadlineTimeout(timeout);
|
|
94
|
+
inboundSignal?.removeEventListener('abort', abortFromInbound);
|
|
95
|
+
};
|
|
96
|
+
const signal = requestController.signal;
|
|
66
97
|
const body = init?.body ? JSON.parse(init.body) : {};
|
|
67
98
|
const model = this.extractModel(url) || body.model || 'claude-sonnet-4-5';
|
|
68
99
|
const think = model.endsWith('-thinking') || !!body.providerOptions?.thinkingConfig || !!body.thinkingConfig;
|
|
@@ -72,128 +103,187 @@ export class RequestHandler {
|
|
|
72
103
|
20000;
|
|
73
104
|
let handlerContext = { retry: 0, forcedRefreshAccountIds: new Set() };
|
|
74
105
|
let consecutiveNullAccounts = 0;
|
|
106
|
+
let streamFailureCount = 0;
|
|
107
|
+
let forcedStreamAccount = null;
|
|
75
108
|
const retryContext = this.retryStrategy.createContext();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!reauthed) {
|
|
84
|
-
throw new Error('All accounts are permanently unhealthy. Please re-authenticate.');
|
|
109
|
+
try {
|
|
110
|
+
while (true) {
|
|
111
|
+
if (signal.aborted)
|
|
112
|
+
throw signal.reason;
|
|
113
|
+
const check = this.retryStrategy.shouldContinue(retryContext);
|
|
114
|
+
if (!check.canContinue) {
|
|
115
|
+
throw new Error(check.error);
|
|
85
116
|
}
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
let acc = await this.accountSelector.selectHealthyAccount(showToast).catch(async (e) => {
|
|
89
|
-
if (e instanceof Error && e.message.includes('reauth required')) {
|
|
117
|
+
if (this.allAccountsPermanentlyUnhealthy()) {
|
|
90
118
|
const reauthed = await this.triggerReauth(showToast);
|
|
91
|
-
if (!reauthed)
|
|
92
|
-
throw new Error('All accounts are unhealthy
|
|
93
|
-
|
|
119
|
+
if (!reauthed) {
|
|
120
|
+
throw new Error('All accounts are permanently unhealthy. Please re-authenticate.');
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
94
123
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (tokenResult.shouldContinue) {
|
|
107
|
-
acc = tokenResult.account;
|
|
108
|
-
await this.sleep(500);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
const sdkPrep = this.prepareSdkRequest(init?.body, model, auth, think, budget, showToast);
|
|
112
|
-
if (this.config.enable_log_effort_debug) {
|
|
113
|
-
try {
|
|
114
|
-
logger.log('[effort-debug] request effort resolution', {
|
|
115
|
-
model,
|
|
116
|
-
effectiveModel: sdkPrep.effectiveModel,
|
|
117
|
-
think,
|
|
118
|
-
budget,
|
|
119
|
-
resolvedEffort: sdkPrep.effort ?? 'undefined (not effort-capable or not thinking)',
|
|
120
|
-
inboundBodyKeys: Object.keys(body),
|
|
121
|
-
messagesCount: body.messages?.length,
|
|
122
|
-
reasoningSubtree: {
|
|
123
|
-
reasoningEffort: body.reasoningEffort,
|
|
124
|
-
reasoning_effort: body.reasoning_effort,
|
|
125
|
-
reasoning: body.reasoning,
|
|
126
|
-
providerOptions: body.providerOptions,
|
|
127
|
-
thinkingConfig: body.thinkingConfig,
|
|
128
|
-
providerOptionsThinkingConfig: body.providerOptions?.thinkingConfig
|
|
124
|
+
let acc = forcedStreamAccount;
|
|
125
|
+
forcedStreamAccount = null;
|
|
126
|
+
if (!acc) {
|
|
127
|
+
acc = await this.accountSelector
|
|
128
|
+
.selectHealthyAccount(showToast, signal)
|
|
129
|
+
.catch(async (e) => {
|
|
130
|
+
if (e instanceof Error && e.message.includes('reauth required')) {
|
|
131
|
+
const reauthed = await this.triggerReauth(showToast);
|
|
132
|
+
if (!reauthed)
|
|
133
|
+
throw new Error('All accounts are unhealthy or rate-limited. Please re-authenticate.');
|
|
134
|
+
return null;
|
|
129
135
|
}
|
|
136
|
+
throw e;
|
|
130
137
|
});
|
|
131
138
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
if (!acc) {
|
|
140
|
+
consecutiveNullAccounts++;
|
|
141
|
+
const backoffDelay = Math.min(1000 * Math.pow(2, consecutiveNullAccounts - 1), 10000);
|
|
142
|
+
await this.sleep(backoffDelay, signal);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
consecutiveNullAccounts = 0;
|
|
146
|
+
const auth = this.accountManager.toAuthDetails(acc);
|
|
147
|
+
const tokenResult = await this.tokenRefresher.refreshIfNeeded(acc, auth, showToast);
|
|
148
|
+
if (tokenResult.shouldContinue) {
|
|
149
|
+
acc = tokenResult.account;
|
|
150
|
+
await this.sleep(500, signal);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const sdkPrep = this.prepareSdkRequest(init?.body, model, auth, think, budget, showToast);
|
|
154
|
+
if (this.config.enable_log_effort_debug) {
|
|
155
|
+
try {
|
|
156
|
+
logger.log('[effort-debug] request effort resolution', {
|
|
157
|
+
model,
|
|
158
|
+
effectiveModel: sdkPrep.effectiveModel,
|
|
159
|
+
think,
|
|
160
|
+
budget,
|
|
161
|
+
resolvedEffort: sdkPrep.effort ?? 'undefined (not effort-capable or not thinking)',
|
|
162
|
+
inboundBodyKeys: Object.keys(body),
|
|
163
|
+
messagesCount: body.messages?.length,
|
|
164
|
+
reasoningSubtree: {
|
|
165
|
+
reasoningEffort: body.reasoningEffort,
|
|
166
|
+
reasoning_effort: body.reasoning_effort,
|
|
167
|
+
reasoning: body.reasoning,
|
|
168
|
+
providerOptions: body.providerOptions,
|
|
169
|
+
thinkingConfig: body.thinkingConfig,
|
|
170
|
+
providerOptionsThinkingConfig: body.providerOptions?.thinkingConfig
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch { }
|
|
175
|
+
}
|
|
176
|
+
const apiTimestamp = this.config.enable_log_api_request ? logger.getTimestamp() : null;
|
|
145
177
|
if (apiTimestamp) {
|
|
146
|
-
this.
|
|
178
|
+
this.logSdkRequest(sdkPrep, acc, apiTimestamp);
|
|
147
179
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
180
|
+
let sendResolved = false;
|
|
181
|
+
try {
|
|
182
|
+
const client = this.makeSdkClient(auth, sdkPrep.region, sdkPrep.effort);
|
|
183
|
+
const command = new GenerateAssistantResponseCommand({
|
|
184
|
+
conversationState: sdkPrep.conversationState,
|
|
185
|
+
profileArn: sdkPrep.profileArn
|
|
186
|
+
});
|
|
187
|
+
const attemptEpoch = this.nextAccountAttemptEpoch(acc.id);
|
|
188
|
+
const isCurrentAttempt = () => this.accountAttemptEpochs.get(acc.id) === attemptEpoch;
|
|
189
|
+
let completionDone = false;
|
|
190
|
+
const completeRequest = async () => {
|
|
191
|
+
if (completionDone)
|
|
192
|
+
return;
|
|
193
|
+
completionDone = true;
|
|
194
|
+
if (!isCurrentAttempt())
|
|
195
|
+
return;
|
|
196
|
+
this.handleSuccessfulRequest(acc);
|
|
197
|
+
await this.usageTracker.syncUsage(acc, auth, isCurrentAttempt);
|
|
198
|
+
};
|
|
199
|
+
const sdkResponse = await client.send(command, { abortSignal: signal });
|
|
200
|
+
sendResolved = true;
|
|
201
|
+
refreshRequestDeadline();
|
|
155
202
|
if (apiTimestamp) {
|
|
156
|
-
this.
|
|
203
|
+
this.logSdkResponse(sdkPrep, apiTimestamp);
|
|
204
|
+
}
|
|
205
|
+
const response = await this.responseHandler.handleSdkSuccess(sdkResponse, model, sdkPrep.conversationId, sdkPrep.streaming, {
|
|
206
|
+
signal,
|
|
207
|
+
onActivity: refreshRequestDeadline,
|
|
208
|
+
onComplete: completeRequest,
|
|
209
|
+
onTerminal: cleanupRequest,
|
|
210
|
+
onCancel: (reason) => requestController.abort(reason),
|
|
211
|
+
mapError: (error) => new UpstreamUnexpectedError(error, true)
|
|
212
|
+
});
|
|
213
|
+
if (sdkPrep.streaming) {
|
|
214
|
+
responseOwnsLifecycle = true;
|
|
157
215
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
216
|
+
else {
|
|
217
|
+
await completeRequest();
|
|
218
|
+
}
|
|
219
|
+
return response;
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
if (signal.aborted)
|
|
223
|
+
throw signal.reason;
|
|
224
|
+
if (e instanceof SdkEventStreamIterationError) {
|
|
225
|
+
streamFailureCount++;
|
|
226
|
+
const streamError = new UpstreamUnexpectedError(e, false);
|
|
227
|
+
if (streamFailureCount >= 3)
|
|
228
|
+
return streamError.toResponse();
|
|
229
|
+
await this.sleep(this.getStreamRetryDelay(streamFailureCount), signal);
|
|
230
|
+
if (streamFailureCount === 1) {
|
|
231
|
+
forcedStreamAccount = acc;
|
|
169
232
|
}
|
|
170
|
-
|
|
171
|
-
|
|
233
|
+
else {
|
|
234
|
+
forcedStreamAccount =
|
|
235
|
+
(await this.accountSelector.selectAlternativeAccount(new Set([acc.id]))) ?? acc;
|
|
172
236
|
}
|
|
173
237
|
continue;
|
|
174
238
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
239
|
+
if (sendResolved)
|
|
240
|
+
throw e;
|
|
241
|
+
const httpStatus = e?.$metadata?.httpStatusCode;
|
|
242
|
+
if (httpStatus) {
|
|
243
|
+
if (apiTimestamp) {
|
|
244
|
+
this.logSdkError(sdkPrep, e, acc, apiTimestamp);
|
|
245
|
+
}
|
|
246
|
+
const errorBody = JSON.stringify({ message: e.message, __type: e.name });
|
|
247
|
+
const errorStatusText = e.name || 'Error';
|
|
248
|
+
const jsonHeaders = { 'Content-Type': 'application/json' };
|
|
249
|
+
const errorResult = await this.errorHandler.handle(e, new Response(errorBody, {
|
|
250
|
+
status: httpStatus,
|
|
251
|
+
statusText: errorStatusText,
|
|
252
|
+
headers: jsonHeaders
|
|
253
|
+
}), acc, handlerContext, showToast, signal);
|
|
254
|
+
if (errorResult.shouldRetry) {
|
|
255
|
+
if (errorResult.newContext) {
|
|
256
|
+
handlerContext = errorResult.newContext;
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
// Terminal, non-retryable HTTP error. Return a fresh Response carrying
|
|
261
|
+
// the real Kiro body so @ai-sdk/openai-compatible produces an
|
|
262
|
+
// APICallError with status+body (not a bare Error that OpenCode
|
|
263
|
+
// degrades to UnknownError). Remap size-overflow 400s to 413 so
|
|
264
|
+
// OpenCode classifies context_overflow and auto-compacts.
|
|
265
|
+
const terminalStatus = httpStatus === 400 && isKiroContextOverflowBody(e.message ?? '') ? 413 : httpStatus;
|
|
266
|
+
return new Response(errorBody, {
|
|
267
|
+
status: terminalStatus,
|
|
268
|
+
statusText: errorStatusText,
|
|
269
|
+
headers: jsonHeaders
|
|
270
|
+
});
|
|
191
271
|
}
|
|
192
|
-
|
|
272
|
+
const networkResult = await this.errorHandler.handleNetworkError(e, handlerContext, showToast, signal);
|
|
273
|
+
if (networkResult.shouldRetry) {
|
|
274
|
+
if (networkResult.newContext) {
|
|
275
|
+
handlerContext = { ...handlerContext, ...networkResult.newContext };
|
|
276
|
+
}
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
throw e;
|
|
193
280
|
}
|
|
194
|
-
throw e;
|
|
195
281
|
}
|
|
196
282
|
}
|
|
283
|
+
finally {
|
|
284
|
+
if (!responseOwnsLifecycle)
|
|
285
|
+
cleanupRequest();
|
|
286
|
+
}
|
|
197
287
|
}
|
|
198
288
|
extractModel(url) {
|
|
199
289
|
return url.match(/models\/([^/:]+)/)?.[1] || null;
|
|
@@ -335,7 +425,29 @@ export class RequestHandler {
|
|
|
335
425
|
}
|
|
336
426
|
return accounts.every((acc) => !acc.isHealthy && isPermanentError(acc.unhealthyReason));
|
|
337
427
|
}
|
|
338
|
-
|
|
339
|
-
|
|
428
|
+
nextAccountAttemptEpoch(accountId) {
|
|
429
|
+
const next = (this.accountAttemptEpochs.get(accountId) ?? 0) + 1;
|
|
430
|
+
this.accountAttemptEpochs.set(accountId, next);
|
|
431
|
+
return next;
|
|
432
|
+
}
|
|
433
|
+
getStreamRetryDelay(failureCount) {
|
|
434
|
+
const base = 250 * Math.pow(2, failureCount - 1);
|
|
435
|
+
return base + base * 0.25 * this.streamRetryRandom();
|
|
436
|
+
}
|
|
437
|
+
sleep(ms, signal) {
|
|
438
|
+
if (signal?.aborted)
|
|
439
|
+
return Promise.reject(signal.reason);
|
|
440
|
+
let onAbort;
|
|
441
|
+
return new Promise((resolve, reject) => {
|
|
442
|
+
const timer = setTimeout(resolve, ms);
|
|
443
|
+
onAbort = () => {
|
|
444
|
+
clearTimeout(timer);
|
|
445
|
+
reject(signal?.reason);
|
|
446
|
+
};
|
|
447
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
448
|
+
}).finally(() => {
|
|
449
|
+
if (onAbort)
|
|
450
|
+
signal?.removeEventListener('abort', onAbort);
|
|
451
|
+
});
|
|
340
452
|
}
|
|
341
453
|
}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import { SdkEventStreamIterationError } from './stream-error.js';
|
|
2
|
+
export interface SdkResponseLifecycle {
|
|
3
|
+
signal?: AbortSignal;
|
|
4
|
+
onActivity?: () => void;
|
|
5
|
+
onComplete?: () => void | Promise<void>;
|
|
6
|
+
onTerminal?: () => void;
|
|
7
|
+
onCancel?: (reason: unknown) => void;
|
|
8
|
+
mapError?: (error: SdkEventStreamIterationError, emittedOutput: true) => unknown;
|
|
9
|
+
}
|
|
1
10
|
export declare class ResponseHandler {
|
|
2
11
|
handleSuccess(response: Response, model: string, conversationId: string, streaming: boolean): Promise<Response>;
|
|
3
|
-
handleSdkSuccess(sdkResponse: any, model: string, conversationId: string, streaming: boolean): Promise<Response>;
|
|
12
|
+
handleSdkSuccess(sdkResponse: any, model: string, conversationId: string, streaming: boolean, lifecycle?: SdkResponseLifecycle): Promise<Response>;
|
|
4
13
|
private handleStreaming;
|
|
5
14
|
private handleSdkStreaming;
|
|
6
|
-
private handleNonStreaming;
|
|
7
15
|
private handleSdkNonStreaming;
|
|
16
|
+
private handleNonStreaming;
|
|
8
17
|
}
|
|
@@ -1,6 +1,93 @@
|
|
|
1
1
|
import { parseEventStream } from '../../plugin/response.js';
|
|
2
2
|
import { transformKiroStream } from '../../plugin/streaming/index.js';
|
|
3
3
|
import { transformSdkStream } from '../../plugin/streaming/sdk-stream-transformer.js';
|
|
4
|
+
import { SdkEventStreamIterationError } from './stream-error.js';
|
|
5
|
+
function abortReason(signal) {
|
|
6
|
+
return signal.reason ?? new DOMException('The request was aborted', 'AbortError');
|
|
7
|
+
}
|
|
8
|
+
async function closeIterator(iterator) {
|
|
9
|
+
try {
|
|
10
|
+
await iterator.return?.();
|
|
11
|
+
}
|
|
12
|
+
catch { }
|
|
13
|
+
}
|
|
14
|
+
function wrapSdkEventStream(sdkResponse, signal, onActivity) {
|
|
15
|
+
const eventStream = sdkResponse.generateAssistantResponseResponse;
|
|
16
|
+
if (!eventStream || typeof eventStream[Symbol.asyncIterator] !== 'function') {
|
|
17
|
+
return { response: sdkResponse, closeRaw: async () => { } };
|
|
18
|
+
}
|
|
19
|
+
const rawIterator = eventStream[Symbol.asyncIterator]();
|
|
20
|
+
let closed = false;
|
|
21
|
+
const closeRaw = async () => {
|
|
22
|
+
if (closed)
|
|
23
|
+
return;
|
|
24
|
+
closed = true;
|
|
25
|
+
await closeIterator(rawIterator);
|
|
26
|
+
};
|
|
27
|
+
const nextRaw = async () => {
|
|
28
|
+
const recordActivity = (result) => {
|
|
29
|
+
if (!result.done)
|
|
30
|
+
onActivity?.();
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
if (!signal)
|
|
34
|
+
return rawIterator.next().then(recordActivity);
|
|
35
|
+
if (signal.aborted) {
|
|
36
|
+
await closeRaw();
|
|
37
|
+
throw abortReason(signal);
|
|
38
|
+
}
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let settled = false;
|
|
41
|
+
const settle = (callback) => {
|
|
42
|
+
if (settled)
|
|
43
|
+
return;
|
|
44
|
+
settled = true;
|
|
45
|
+
signal.removeEventListener('abort', onAbort);
|
|
46
|
+
callback();
|
|
47
|
+
};
|
|
48
|
+
const onAbort = () => {
|
|
49
|
+
void closeRaw();
|
|
50
|
+
settle(() => reject(abortReason(signal)));
|
|
51
|
+
};
|
|
52
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
53
|
+
Promise.resolve(rawIterator.next()).then((result) => settle(() => resolve(recordActivity(result))), (error) => settle(() => reject(error)));
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
const wrappedIterator = {
|
|
57
|
+
async next() {
|
|
58
|
+
try {
|
|
59
|
+
return await nextRaw();
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (signal?.aborted)
|
|
63
|
+
throw abortReason(signal);
|
|
64
|
+
throw new SdkEventStreamIterationError(error);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
async return() {
|
|
68
|
+
await closeRaw();
|
|
69
|
+
return { done: true, value: undefined };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const wrappedStream = {
|
|
73
|
+
[Symbol.asyncIterator]() {
|
|
74
|
+
return wrappedIterator;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
response: { ...sdkResponse, generateAssistantResponseResponse: wrappedStream },
|
|
79
|
+
closeRaw
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function isSemanticChunk(chunk) {
|
|
83
|
+
const delta = chunk?.choices?.[0]?.delta;
|
|
84
|
+
return (delta?.content !== undefined ||
|
|
85
|
+
delta?.reasoning_content !== undefined ||
|
|
86
|
+
delta?.tool_calls !== undefined);
|
|
87
|
+
}
|
|
88
|
+
function encodeSseChunk(chunk) {
|
|
89
|
+
return new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
90
|
+
}
|
|
4
91
|
export class ResponseHandler {
|
|
5
92
|
async handleSuccess(response, model, conversationId, streaming) {
|
|
6
93
|
if (streaming) {
|
|
@@ -8,11 +95,11 @@ export class ResponseHandler {
|
|
|
8
95
|
}
|
|
9
96
|
return this.handleNonStreaming(response, model, conversationId);
|
|
10
97
|
}
|
|
11
|
-
async handleSdkSuccess(sdkResponse, model, conversationId, streaming) {
|
|
98
|
+
async handleSdkSuccess(sdkResponse, model, conversationId, streaming, lifecycle = {}) {
|
|
12
99
|
if (streaming) {
|
|
13
|
-
return this.handleSdkStreaming(sdkResponse, model, conversationId);
|
|
100
|
+
return this.handleSdkStreaming(sdkResponse, model, conversationId, lifecycle);
|
|
14
101
|
}
|
|
15
|
-
return this.handleSdkNonStreaming(sdkResponse, model, conversationId);
|
|
102
|
+
return this.handleSdkNonStreaming(sdkResponse, model, conversationId, lifecycle);
|
|
16
103
|
}
|
|
17
104
|
async handleStreaming(response, model, conversationId) {
|
|
18
105
|
const s = transformKiroStream(response, model, conversationId);
|
|
@@ -30,25 +117,145 @@ export class ResponseHandler {
|
|
|
30
117
|
}
|
|
31
118
|
}), { headers: { 'Content-Type': 'text/event-stream' } });
|
|
32
119
|
}
|
|
33
|
-
async handleSdkStreaming(sdkResponse, model, conversationId) {
|
|
34
|
-
const
|
|
120
|
+
async handleSdkStreaming(sdkResponse, model, conversationId, lifecycle) {
|
|
121
|
+
const wrapped = wrapSdkEventStream(sdkResponse, lifecycle.signal, lifecycle.onActivity);
|
|
122
|
+
const transformed = transformSdkStream(wrapped.response, model, conversationId);
|
|
123
|
+
const buffered = [];
|
|
124
|
+
let firstSemantic;
|
|
125
|
+
while (true) {
|
|
126
|
+
const item = await transformed.next();
|
|
127
|
+
if (item.done) {
|
|
128
|
+
await lifecycle.onComplete?.();
|
|
129
|
+
lifecycle.onTerminal?.();
|
|
130
|
+
let index = 0;
|
|
131
|
+
return new Response(new ReadableStream({
|
|
132
|
+
pull(controller) {
|
|
133
|
+
const chunk = buffered[index++];
|
|
134
|
+
if (chunk)
|
|
135
|
+
controller.enqueue(chunk);
|
|
136
|
+
if (index >= buffered.length)
|
|
137
|
+
controller.close();
|
|
138
|
+
}
|
|
139
|
+
}, { highWaterMark: 0 }), { headers: { 'Content-Type': 'text/event-stream' } });
|
|
140
|
+
}
|
|
141
|
+
const encoded = encodeSseChunk(item.value);
|
|
142
|
+
if (isSemanticChunk(item.value)) {
|
|
143
|
+
firstSemantic = encoded;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
buffered.push(encoded);
|
|
147
|
+
}
|
|
148
|
+
let terminal = false;
|
|
149
|
+
let firstPull = true;
|
|
150
|
+
let abortListener;
|
|
151
|
+
const finish = () => {
|
|
152
|
+
if (terminal)
|
|
153
|
+
return;
|
|
154
|
+
terminal = true;
|
|
155
|
+
if (abortListener && lifecycle.signal) {
|
|
156
|
+
lifecycle.signal.removeEventListener('abort', abortListener);
|
|
157
|
+
}
|
|
158
|
+
lifecycle.onTerminal?.();
|
|
159
|
+
};
|
|
160
|
+
const cleanupIterators = async () => {
|
|
161
|
+
try {
|
|
162
|
+
await transformed.return(undefined);
|
|
163
|
+
}
|
|
164
|
+
catch { }
|
|
165
|
+
await wrapped.closeRaw();
|
|
166
|
+
};
|
|
35
167
|
return new Response(new ReadableStream({
|
|
36
|
-
|
|
168
|
+
start(controller) {
|
|
169
|
+
if (!lifecycle.signal)
|
|
170
|
+
return;
|
|
171
|
+
abortListener = () => {
|
|
172
|
+
if (terminal)
|
|
173
|
+
return;
|
|
174
|
+
const reason = abortReason(lifecycle.signal);
|
|
175
|
+
void cleanupIterators();
|
|
176
|
+
finish();
|
|
177
|
+
controller.error(reason);
|
|
178
|
+
};
|
|
179
|
+
if (lifecycle.signal.aborted)
|
|
180
|
+
abortListener();
|
|
181
|
+
else
|
|
182
|
+
lifecycle.signal.addEventListener('abort', abortListener, { once: true });
|
|
183
|
+
},
|
|
184
|
+
async pull(controller) {
|
|
185
|
+
if (terminal)
|
|
186
|
+
return;
|
|
187
|
+
if (firstPull) {
|
|
188
|
+
firstPull = false;
|
|
189
|
+
for (const chunk of buffered)
|
|
190
|
+
controller.enqueue(chunk);
|
|
191
|
+
controller.enqueue(firstSemantic);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
37
194
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
195
|
+
const item = await transformed.next();
|
|
196
|
+
if (item.done) {
|
|
197
|
+
await lifecycle.onComplete?.();
|
|
198
|
+
finish();
|
|
199
|
+
controller.close();
|
|
200
|
+
return;
|
|
40
201
|
}
|
|
41
|
-
|
|
202
|
+
controller.enqueue(encodeSseChunk(item.value));
|
|
42
203
|
}
|
|
43
|
-
catch (
|
|
44
|
-
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (terminal)
|
|
206
|
+
return;
|
|
207
|
+
const mapped = error instanceof SdkEventStreamIterationError
|
|
208
|
+
? (lifecycle.mapError?.(error, true) ?? error)
|
|
209
|
+
: error;
|
|
210
|
+
finish();
|
|
211
|
+
controller.error(mapped);
|
|
45
212
|
}
|
|
213
|
+
},
|
|
214
|
+
async cancel(reason) {
|
|
215
|
+
if (terminal)
|
|
216
|
+
return;
|
|
217
|
+
finish();
|
|
218
|
+
lifecycle.onCancel?.(reason);
|
|
219
|
+
await cleanupIterators();
|
|
46
220
|
}
|
|
47
|
-
}), { headers: { 'Content-Type': 'text/event-stream' } });
|
|
221
|
+
}, { highWaterMark: 0 }), { headers: { 'Content-Type': 'text/event-stream' } });
|
|
48
222
|
}
|
|
49
|
-
async
|
|
50
|
-
|
|
51
|
-
|
|
223
|
+
async handleSdkNonStreaming(sdkResponse, model, conversationId, lifecycle) {
|
|
224
|
+
// For non-streaming SDK responses, collect all events
|
|
225
|
+
let content = '';
|
|
226
|
+
let reasoningContent = '';
|
|
227
|
+
const toolCalls = [];
|
|
228
|
+
let inputTokens = 0;
|
|
229
|
+
let outputTokens = 0;
|
|
230
|
+
const wrapped = wrapSdkEventStream(sdkResponse, lifecycle.signal, lifecycle.onActivity);
|
|
231
|
+
const eventStream = wrapped.response.generateAssistantResponseResponse;
|
|
232
|
+
if (eventStream) {
|
|
233
|
+
try {
|
|
234
|
+
for await (const event of eventStream) {
|
|
235
|
+
if (event.reasoningContentEvent?.text) {
|
|
236
|
+
reasoningContent += event.reasoningContentEvent.text;
|
|
237
|
+
}
|
|
238
|
+
if (event.assistantResponseEvent?.content) {
|
|
239
|
+
content += event.assistantResponseEvent.content;
|
|
240
|
+
}
|
|
241
|
+
if (event.toolUseEvent) {
|
|
242
|
+
toolCalls.push(event.toolUseEvent);
|
|
243
|
+
}
|
|
244
|
+
if (event.metadataEvent?.tokenUsage) {
|
|
245
|
+
inputTokens = event.metadataEvent.tokenUsage.inputTokens || 0;
|
|
246
|
+
outputTokens = event.metadataEvent.tokenUsage.outputTokens || 0;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
if (lifecycle.signal?.aborted)
|
|
252
|
+
await wrapped.closeRaw();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const message = { role: 'assistant', content };
|
|
256
|
+
if (reasoningContent) {
|
|
257
|
+
message.reasoning_content = reasoningContent;
|
|
258
|
+
}
|
|
52
259
|
const oai = {
|
|
53
260
|
id: conversationId,
|
|
54
261
|
object: 'chat.completion',
|
|
@@ -57,18 +264,18 @@ export class ResponseHandler {
|
|
|
57
264
|
choices: [
|
|
58
265
|
{
|
|
59
266
|
index: 0,
|
|
60
|
-
message
|
|
61
|
-
finish_reason:
|
|
267
|
+
message,
|
|
268
|
+
finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop'
|
|
62
269
|
}
|
|
63
270
|
],
|
|
64
271
|
usage: {
|
|
65
|
-
prompt_tokens:
|
|
66
|
-
completion_tokens:
|
|
67
|
-
total_tokens:
|
|
272
|
+
prompt_tokens: inputTokens,
|
|
273
|
+
completion_tokens: outputTokens,
|
|
274
|
+
total_tokens: inputTokens + outputTokens
|
|
68
275
|
}
|
|
69
276
|
};
|
|
70
|
-
if (
|
|
71
|
-
oai.choices[0].message.tool_calls =
|
|
277
|
+
if (toolCalls.length > 0) {
|
|
278
|
+
oai.choices[0].message.tool_calls = toolCalls.map((tc) => ({
|
|
72
279
|
id: tc.toolUseId,
|
|
73
280
|
type: 'function',
|
|
74
281
|
function: {
|
|
@@ -81,35 +288,9 @@ export class ResponseHandler {
|
|
|
81
288
|
headers: { 'Content-Type': 'application/json' }
|
|
82
289
|
});
|
|
83
290
|
}
|
|
84
|
-
async
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let reasoningContent = '';
|
|
88
|
-
const toolCalls = [];
|
|
89
|
-
let inputTokens = 0;
|
|
90
|
-
let outputTokens = 0;
|
|
91
|
-
const eventStream = sdkResponse.generateAssistantResponseResponse;
|
|
92
|
-
if (eventStream) {
|
|
93
|
-
for await (const event of eventStream) {
|
|
94
|
-
if (event.reasoningContentEvent?.text) {
|
|
95
|
-
reasoningContent += event.reasoningContentEvent.text;
|
|
96
|
-
}
|
|
97
|
-
if (event.assistantResponseEvent?.content) {
|
|
98
|
-
content += event.assistantResponseEvent.content;
|
|
99
|
-
}
|
|
100
|
-
if (event.toolUseEvent) {
|
|
101
|
-
toolCalls.push(event.toolUseEvent);
|
|
102
|
-
}
|
|
103
|
-
if (event.metadataEvent?.tokenUsage) {
|
|
104
|
-
inputTokens = event.metadataEvent.tokenUsage.inputTokens || 0;
|
|
105
|
-
outputTokens = event.metadataEvent.tokenUsage.outputTokens || 0;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const message = { role: 'assistant', content };
|
|
110
|
-
if (reasoningContent) {
|
|
111
|
-
message.reasoning_content = reasoningContent;
|
|
112
|
-
}
|
|
291
|
+
async handleNonStreaming(response, model, conversationId) {
|
|
292
|
+
const text = await response.text();
|
|
293
|
+
const p = parseEventStream(text, model);
|
|
113
294
|
const oai = {
|
|
114
295
|
id: conversationId,
|
|
115
296
|
object: 'chat.completion',
|
|
@@ -118,18 +299,18 @@ export class ResponseHandler {
|
|
|
118
299
|
choices: [
|
|
119
300
|
{
|
|
120
301
|
index: 0,
|
|
121
|
-
message,
|
|
122
|
-
finish_reason:
|
|
302
|
+
message: { role: 'assistant', content: p.content },
|
|
303
|
+
finish_reason: p.stopReason === 'tool_use' ? 'tool_calls' : 'stop'
|
|
123
304
|
}
|
|
124
305
|
],
|
|
125
306
|
usage: {
|
|
126
|
-
prompt_tokens: inputTokens,
|
|
127
|
-
completion_tokens: outputTokens,
|
|
128
|
-
total_tokens: inputTokens + outputTokens
|
|
307
|
+
prompt_tokens: p.inputTokens || 0,
|
|
308
|
+
completion_tokens: p.outputTokens || 0,
|
|
309
|
+
total_tokens: (p.inputTokens || 0) + (p.outputTokens || 0)
|
|
129
310
|
}
|
|
130
311
|
};
|
|
131
|
-
if (toolCalls.length > 0) {
|
|
132
|
-
oai.choices[0].message.tool_calls = toolCalls.map((tc) => ({
|
|
312
|
+
if (p.toolCalls.length > 0) {
|
|
313
|
+
oai.choices[0].message.tool_calls = p.toolCalls.map((tc) => ({
|
|
133
314
|
id: tc.toolUseId,
|
|
134
315
|
type: 'function',
|
|
135
316
|
function: {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface UpstreamUnexpectedPayload {
|
|
2
|
+
retryable: true;
|
|
3
|
+
phase: 'stream';
|
|
4
|
+
emittedOutput: boolean;
|
|
5
|
+
code: 'UPSTREAM_UNEXPECTED';
|
|
6
|
+
}
|
|
7
|
+
export declare class SdkEventStreamIterationError extends Error {
|
|
8
|
+
readonly name = "SdkEventStreamIterationError";
|
|
9
|
+
constructor(cause: unknown);
|
|
10
|
+
}
|
|
11
|
+
export declare class UpstreamUnexpectedError extends Error {
|
|
12
|
+
readonly emittedOutput: boolean;
|
|
13
|
+
readonly name = "UpstreamUnexpectedError";
|
|
14
|
+
readonly retryable = true;
|
|
15
|
+
readonly phase = "stream";
|
|
16
|
+
readonly code = "UPSTREAM_UNEXPECTED";
|
|
17
|
+
constructor(cause: unknown, emittedOutput: boolean);
|
|
18
|
+
toPayload(): UpstreamUnexpectedPayload;
|
|
19
|
+
toResponse(): Response;
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class SdkEventStreamIterationError extends Error {
|
|
2
|
+
name = 'SdkEventStreamIterationError';
|
|
3
|
+
constructor(cause) {
|
|
4
|
+
super('Kiro SDK event stream iteration failed', { cause });
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class UpstreamUnexpectedError extends Error {
|
|
8
|
+
emittedOutput;
|
|
9
|
+
name = 'UpstreamUnexpectedError';
|
|
10
|
+
retryable = true;
|
|
11
|
+
phase = 'stream';
|
|
12
|
+
code = 'UPSTREAM_UNEXPECTED';
|
|
13
|
+
constructor(cause, emittedOutput) {
|
|
14
|
+
super('Kiro upstream event stream failed unexpectedly', { cause });
|
|
15
|
+
this.emittedOutput = emittedOutput;
|
|
16
|
+
}
|
|
17
|
+
toPayload() {
|
|
18
|
+
return {
|
|
19
|
+
retryable: true,
|
|
20
|
+
phase: 'stream',
|
|
21
|
+
emittedOutput: this.emittedOutput,
|
|
22
|
+
code: 'UPSTREAM_UNEXPECTED'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
toResponse() {
|
|
26
|
+
return new Response(JSON.stringify(this.toPayload()), {
|
|
27
|
+
status: 503,
|
|
28
|
+
headers: { 'Content-Type': 'application/json' }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -36,7 +36,10 @@ export declare class AccountManager {
|
|
|
36
36
|
shouldShowUsageToast(debounce?: number): boolean;
|
|
37
37
|
getMinWaitTime(): number;
|
|
38
38
|
allSelectableBlockedByOverage(): boolean;
|
|
39
|
-
getCurrentOrNext(
|
|
39
|
+
getCurrentOrNext(options?: {
|
|
40
|
+
excludedIds?: ReadonlySet<string>;
|
|
41
|
+
recoverUnhealthy?: boolean;
|
|
42
|
+
}): ManagedAccount | null;
|
|
40
43
|
updateUsage(id: string, meta: {
|
|
41
44
|
usedCount: number;
|
|
42
45
|
limitCount: number;
|
package/dist/plugin/accounts.js
CHANGED
|
@@ -121,13 +121,19 @@ export class AccountManager {
|
|
|
121
121
|
const hasHealthySelectable = this.accounts.some((a) => a.isHealthy && !rateLimited(a) && !this.isOverageBlocked(a));
|
|
122
122
|
return hasOverageBlockedEligible && !hasCleanRateLimited && !hasHealthySelectable;
|
|
123
123
|
}
|
|
124
|
-
getCurrentOrNext() {
|
|
124
|
+
getCurrentOrNext(options = {}) {
|
|
125
125
|
const now = Date.now();
|
|
126
|
+
const excludedIds = options.excludedIds ?? new Set();
|
|
127
|
+
const recoverUnhealthy = options.recoverUnhealthy ?? true;
|
|
126
128
|
const overageBlocked = (a) => this.isOverageBlocked(a);
|
|
127
129
|
const available = this.accounts.filter((a) => {
|
|
130
|
+
if (excludedIds.has(a.id))
|
|
131
|
+
return false;
|
|
128
132
|
if (overageBlocked(a))
|
|
129
133
|
return false;
|
|
130
134
|
if (!a.isHealthy) {
|
|
135
|
+
if (!recoverUnhealthy)
|
|
136
|
+
return false;
|
|
131
137
|
if (isPermanentError(a.unhealthyReason)) {
|
|
132
138
|
return false;
|
|
133
139
|
}
|
|
@@ -205,9 +211,10 @@ export class AccountManager {
|
|
|
205
211
|
})[0];
|
|
206
212
|
}
|
|
207
213
|
}
|
|
208
|
-
if (!selected) {
|
|
214
|
+
if (!selected && recoverUnhealthy) {
|
|
209
215
|
const fallback = this.accounts
|
|
210
|
-
.filter((a) => !a.
|
|
216
|
+
.filter((a) => !excludedIds.has(a.id) &&
|
|
217
|
+
!a.isHealthy &&
|
|
211
218
|
a.failCount < 10 &&
|
|
212
219
|
!isPermanentError(a.unhealthyReason) &&
|
|
213
220
|
!overageBlocked(a))
|