@sunerpy/opencode-kiro-auth 0.13.1 → 0.13.2
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 +206 -105
- package/dist/core/request/response-handler.d.ts +10 -2
- package/dist/core/request/response-handler.js +234 -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,24 @@ 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
|
+
const timeout = setDeadlineTimeout(() => requestController.abort(new DOMException('Kiro request timed out', 'TimeoutError')), this.config.request_timeout_ms);
|
|
78
|
+
let requestCleanupDone = false;
|
|
79
|
+
let responseOwnsLifecycle = false;
|
|
80
|
+
const cleanupRequest = () => {
|
|
81
|
+
if (requestCleanupDone)
|
|
82
|
+
return;
|
|
83
|
+
requestCleanupDone = true;
|
|
84
|
+
clearDeadlineTimeout(timeout);
|
|
85
|
+
inboundSignal?.removeEventListener('abort', abortFromInbound);
|
|
86
|
+
};
|
|
87
|
+
const signal = requestController.signal;
|
|
66
88
|
const body = init?.body ? JSON.parse(init.body) : {};
|
|
67
89
|
const model = this.extractModel(url) || body.model || 'claude-sonnet-4-5';
|
|
68
90
|
const think = model.endsWith('-thinking') || !!body.providerOptions?.thinkingConfig || !!body.thinkingConfig;
|
|
@@ -72,128 +94,185 @@ export class RequestHandler {
|
|
|
72
94
|
20000;
|
|
73
95
|
let handlerContext = { retry: 0, forcedRefreshAccountIds: new Set() };
|
|
74
96
|
let consecutiveNullAccounts = 0;
|
|
97
|
+
let streamFailureCount = 0;
|
|
98
|
+
let forcedStreamAccount = null;
|
|
75
99
|
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.');
|
|
100
|
+
try {
|
|
101
|
+
while (true) {
|
|
102
|
+
if (signal.aborted)
|
|
103
|
+
throw signal.reason;
|
|
104
|
+
const check = this.retryStrategy.shouldContinue(retryContext);
|
|
105
|
+
if (!check.canContinue) {
|
|
106
|
+
throw new Error(check.error);
|
|
85
107
|
}
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
let acc = await this.accountSelector.selectHealthyAccount(showToast).catch(async (e) => {
|
|
89
|
-
if (e instanceof Error && e.message.includes('reauth required')) {
|
|
108
|
+
if (this.allAccountsPermanentlyUnhealthy()) {
|
|
90
109
|
const reauthed = await this.triggerReauth(showToast);
|
|
91
|
-
if (!reauthed)
|
|
92
|
-
throw new Error('All accounts are unhealthy
|
|
93
|
-
|
|
110
|
+
if (!reauthed) {
|
|
111
|
+
throw new Error('All accounts are permanently unhealthy. Please re-authenticate.');
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
94
114
|
}
|
|
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
|
|
115
|
+
let acc = forcedStreamAccount;
|
|
116
|
+
forcedStreamAccount = null;
|
|
117
|
+
if (!acc) {
|
|
118
|
+
acc = await this.accountSelector
|
|
119
|
+
.selectHealthyAccount(showToast, signal)
|
|
120
|
+
.catch(async (e) => {
|
|
121
|
+
if (e instanceof Error && e.message.includes('reauth required')) {
|
|
122
|
+
const reauthed = await this.triggerReauth(showToast);
|
|
123
|
+
if (!reauthed)
|
|
124
|
+
throw new Error('All accounts are unhealthy or rate-limited. Please re-authenticate.');
|
|
125
|
+
return null;
|
|
129
126
|
}
|
|
127
|
+
throw e;
|
|
130
128
|
});
|
|
131
129
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
130
|
+
if (!acc) {
|
|
131
|
+
consecutiveNullAccounts++;
|
|
132
|
+
const backoffDelay = Math.min(1000 * Math.pow(2, consecutiveNullAccounts - 1), 10000);
|
|
133
|
+
await this.sleep(backoffDelay, signal);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
consecutiveNullAccounts = 0;
|
|
137
|
+
const auth = this.accountManager.toAuthDetails(acc);
|
|
138
|
+
const tokenResult = await this.tokenRefresher.refreshIfNeeded(acc, auth, showToast);
|
|
139
|
+
if (tokenResult.shouldContinue) {
|
|
140
|
+
acc = tokenResult.account;
|
|
141
|
+
await this.sleep(500, signal);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const sdkPrep = this.prepareSdkRequest(init?.body, model, auth, think, budget, showToast);
|
|
145
|
+
if (this.config.enable_log_effort_debug) {
|
|
146
|
+
try {
|
|
147
|
+
logger.log('[effort-debug] request effort resolution', {
|
|
148
|
+
model,
|
|
149
|
+
effectiveModel: sdkPrep.effectiveModel,
|
|
150
|
+
think,
|
|
151
|
+
budget,
|
|
152
|
+
resolvedEffort: sdkPrep.effort ?? 'undefined (not effort-capable or not thinking)',
|
|
153
|
+
inboundBodyKeys: Object.keys(body),
|
|
154
|
+
messagesCount: body.messages?.length,
|
|
155
|
+
reasoningSubtree: {
|
|
156
|
+
reasoningEffort: body.reasoningEffort,
|
|
157
|
+
reasoning_effort: body.reasoning_effort,
|
|
158
|
+
reasoning: body.reasoning,
|
|
159
|
+
providerOptions: body.providerOptions,
|
|
160
|
+
thinkingConfig: body.thinkingConfig,
|
|
161
|
+
providerOptionsThinkingConfig: body.providerOptions?.thinkingConfig
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch { }
|
|
166
|
+
}
|
|
167
|
+
const apiTimestamp = this.config.enable_log_api_request ? logger.getTimestamp() : null;
|
|
145
168
|
if (apiTimestamp) {
|
|
146
|
-
this.
|
|
169
|
+
this.logSdkRequest(sdkPrep, acc, apiTimestamp);
|
|
147
170
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
171
|
+
let sendResolved = false;
|
|
172
|
+
try {
|
|
173
|
+
const client = this.makeSdkClient(auth, sdkPrep.region, sdkPrep.effort);
|
|
174
|
+
const command = new GenerateAssistantResponseCommand({
|
|
175
|
+
conversationState: sdkPrep.conversationState,
|
|
176
|
+
profileArn: sdkPrep.profileArn
|
|
177
|
+
});
|
|
178
|
+
const attemptEpoch = this.nextAccountAttemptEpoch(acc.id);
|
|
179
|
+
const isCurrentAttempt = () => this.accountAttemptEpochs.get(acc.id) === attemptEpoch;
|
|
180
|
+
let completionDone = false;
|
|
181
|
+
const completeRequest = async () => {
|
|
182
|
+
if (completionDone)
|
|
183
|
+
return;
|
|
184
|
+
completionDone = true;
|
|
185
|
+
if (!isCurrentAttempt())
|
|
186
|
+
return;
|
|
187
|
+
this.handleSuccessfulRequest(acc);
|
|
188
|
+
await this.usageTracker.syncUsage(acc, auth, isCurrentAttempt);
|
|
189
|
+
};
|
|
190
|
+
const sdkResponse = await client.send(command, { abortSignal: signal });
|
|
191
|
+
sendResolved = true;
|
|
155
192
|
if (apiTimestamp) {
|
|
156
|
-
this.
|
|
193
|
+
this.logSdkResponse(sdkPrep, apiTimestamp);
|
|
194
|
+
}
|
|
195
|
+
const response = await this.responseHandler.handleSdkSuccess(sdkResponse, model, sdkPrep.conversationId, sdkPrep.streaming, {
|
|
196
|
+
signal,
|
|
197
|
+
onComplete: completeRequest,
|
|
198
|
+
onTerminal: cleanupRequest,
|
|
199
|
+
onCancel: (reason) => requestController.abort(reason),
|
|
200
|
+
mapError: (error) => new UpstreamUnexpectedError(error, true)
|
|
201
|
+
});
|
|
202
|
+
if (sdkPrep.streaming) {
|
|
203
|
+
responseOwnsLifecycle = true;
|
|
157
204
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
205
|
+
else {
|
|
206
|
+
await completeRequest();
|
|
207
|
+
}
|
|
208
|
+
return response;
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
if (signal.aborted)
|
|
212
|
+
throw signal.reason;
|
|
213
|
+
if (e instanceof SdkEventStreamIterationError) {
|
|
214
|
+
streamFailureCount++;
|
|
215
|
+
const streamError = new UpstreamUnexpectedError(e, false);
|
|
216
|
+
if (streamFailureCount >= 3)
|
|
217
|
+
return streamError.toResponse();
|
|
218
|
+
await this.sleep(this.getStreamRetryDelay(streamFailureCount), signal);
|
|
219
|
+
if (streamFailureCount === 1) {
|
|
220
|
+
forcedStreamAccount = acc;
|
|
169
221
|
}
|
|
170
|
-
|
|
171
|
-
|
|
222
|
+
else {
|
|
223
|
+
forcedStreamAccount =
|
|
224
|
+
(await this.accountSelector.selectAlternativeAccount(new Set([acc.id]))) ?? acc;
|
|
172
225
|
}
|
|
173
226
|
continue;
|
|
174
227
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
228
|
+
if (sendResolved)
|
|
229
|
+
throw e;
|
|
230
|
+
const httpStatus = e?.$metadata?.httpStatusCode;
|
|
231
|
+
if (httpStatus) {
|
|
232
|
+
if (apiTimestamp) {
|
|
233
|
+
this.logSdkError(sdkPrep, e, acc, apiTimestamp);
|
|
234
|
+
}
|
|
235
|
+
const errorBody = JSON.stringify({ message: e.message, __type: e.name });
|
|
236
|
+
const errorStatusText = e.name || 'Error';
|
|
237
|
+
const jsonHeaders = { 'Content-Type': 'application/json' };
|
|
238
|
+
const errorResult = await this.errorHandler.handle(e, new Response(errorBody, {
|
|
239
|
+
status: httpStatus,
|
|
240
|
+
statusText: errorStatusText,
|
|
241
|
+
headers: jsonHeaders
|
|
242
|
+
}), acc, handlerContext, showToast, signal);
|
|
243
|
+
if (errorResult.shouldRetry) {
|
|
244
|
+
if (errorResult.newContext) {
|
|
245
|
+
handlerContext = errorResult.newContext;
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
// Terminal, non-retryable HTTP error. Return a fresh Response carrying
|
|
250
|
+
// the real Kiro body so @ai-sdk/openai-compatible produces an
|
|
251
|
+
// APICallError with status+body (not a bare Error that OpenCode
|
|
252
|
+
// degrades to UnknownError). Remap size-overflow 400s to 413 so
|
|
253
|
+
// OpenCode classifies context_overflow and auto-compacts.
|
|
254
|
+
const terminalStatus = httpStatus === 400 && isKiroContextOverflowBody(e.message ?? '') ? 413 : httpStatus;
|
|
255
|
+
return new Response(errorBody, {
|
|
256
|
+
status: terminalStatus,
|
|
257
|
+
statusText: errorStatusText,
|
|
258
|
+
headers: jsonHeaders
|
|
259
|
+
});
|
|
191
260
|
}
|
|
192
|
-
|
|
261
|
+
const networkResult = await this.errorHandler.handleNetworkError(e, handlerContext, showToast, signal);
|
|
262
|
+
if (networkResult.shouldRetry) {
|
|
263
|
+
if (networkResult.newContext) {
|
|
264
|
+
handlerContext = { ...handlerContext, ...networkResult.newContext };
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
throw e;
|
|
193
269
|
}
|
|
194
|
-
throw e;
|
|
195
270
|
}
|
|
196
271
|
}
|
|
272
|
+
finally {
|
|
273
|
+
if (!responseOwnsLifecycle)
|
|
274
|
+
cleanupRequest();
|
|
275
|
+
}
|
|
197
276
|
}
|
|
198
277
|
extractModel(url) {
|
|
199
278
|
return url.match(/models\/([^/:]+)/)?.[1] || null;
|
|
@@ -335,7 +414,29 @@ export class RequestHandler {
|
|
|
335
414
|
}
|
|
336
415
|
return accounts.every((acc) => !acc.isHealthy && isPermanentError(acc.unhealthyReason));
|
|
337
416
|
}
|
|
338
|
-
|
|
339
|
-
|
|
417
|
+
nextAccountAttemptEpoch(accountId) {
|
|
418
|
+
const next = (this.accountAttemptEpochs.get(accountId) ?? 0) + 1;
|
|
419
|
+
this.accountAttemptEpochs.set(accountId, next);
|
|
420
|
+
return next;
|
|
421
|
+
}
|
|
422
|
+
getStreamRetryDelay(failureCount) {
|
|
423
|
+
const base = 250 * Math.pow(2, failureCount - 1);
|
|
424
|
+
return base + base * 0.25 * this.streamRetryRandom();
|
|
425
|
+
}
|
|
426
|
+
sleep(ms, signal) {
|
|
427
|
+
if (signal?.aborted)
|
|
428
|
+
return Promise.reject(signal.reason);
|
|
429
|
+
let onAbort;
|
|
430
|
+
return new Promise((resolve, reject) => {
|
|
431
|
+
const timer = setTimeout(resolve, ms);
|
|
432
|
+
onAbort = () => {
|
|
433
|
+
clearTimeout(timer);
|
|
434
|
+
reject(signal?.reason);
|
|
435
|
+
};
|
|
436
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
437
|
+
}).finally(() => {
|
|
438
|
+
if (onAbort)
|
|
439
|
+
signal?.removeEventListener('abort', onAbort);
|
|
440
|
+
});
|
|
340
441
|
}
|
|
341
442
|
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { SdkEventStreamIterationError } from './stream-error.js';
|
|
2
|
+
export interface SdkResponseLifecycle {
|
|
3
|
+
signal?: AbortSignal;
|
|
4
|
+
onComplete?: () => void | Promise<void>;
|
|
5
|
+
onTerminal?: () => void;
|
|
6
|
+
onCancel?: (reason: unknown) => void;
|
|
7
|
+
mapError?: (error: SdkEventStreamIterationError, emittedOutput: true) => unknown;
|
|
8
|
+
}
|
|
1
9
|
export declare class ResponseHandler {
|
|
2
10
|
handleSuccess(response: Response, model: string, conversationId: string, streaming: boolean): Promise<Response>;
|
|
3
|
-
handleSdkSuccess(sdkResponse: any, model: string, conversationId: string, streaming: boolean): Promise<Response>;
|
|
11
|
+
handleSdkSuccess(sdkResponse: any, model: string, conversationId: string, streaming: boolean, lifecycle?: SdkResponseLifecycle): Promise<Response>;
|
|
4
12
|
private handleStreaming;
|
|
5
13
|
private handleSdkStreaming;
|
|
6
|
-
private handleNonStreaming;
|
|
7
14
|
private handleSdkNonStreaming;
|
|
15
|
+
private handleNonStreaming;
|
|
8
16
|
}
|
|
@@ -1,6 +1,88 @@
|
|
|
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) {
|
|
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
|
+
if (!signal)
|
|
29
|
+
return rawIterator.next();
|
|
30
|
+
if (signal.aborted) {
|
|
31
|
+
await closeRaw();
|
|
32
|
+
throw abortReason(signal);
|
|
33
|
+
}
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
let settled = false;
|
|
36
|
+
const settle = (callback) => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
signal.removeEventListener('abort', onAbort);
|
|
41
|
+
callback();
|
|
42
|
+
};
|
|
43
|
+
const onAbort = () => {
|
|
44
|
+
void closeRaw();
|
|
45
|
+
settle(() => reject(abortReason(signal)));
|
|
46
|
+
};
|
|
47
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
48
|
+
Promise.resolve(rawIterator.next()).then((result) => settle(() => resolve(result)), (error) => settle(() => reject(error)));
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const wrappedIterator = {
|
|
52
|
+
async next() {
|
|
53
|
+
try {
|
|
54
|
+
return await nextRaw();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (signal?.aborted)
|
|
58
|
+
throw abortReason(signal);
|
|
59
|
+
throw new SdkEventStreamIterationError(error);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
async return() {
|
|
63
|
+
await closeRaw();
|
|
64
|
+
return { done: true, value: undefined };
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const wrappedStream = {
|
|
68
|
+
[Symbol.asyncIterator]() {
|
|
69
|
+
return wrappedIterator;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
response: { ...sdkResponse, generateAssistantResponseResponse: wrappedStream },
|
|
74
|
+
closeRaw
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function isSemanticChunk(chunk) {
|
|
78
|
+
const delta = chunk?.choices?.[0]?.delta;
|
|
79
|
+
return (delta?.content !== undefined ||
|
|
80
|
+
delta?.reasoning_content !== undefined ||
|
|
81
|
+
delta?.tool_calls !== undefined);
|
|
82
|
+
}
|
|
83
|
+
function encodeSseChunk(chunk) {
|
|
84
|
+
return new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
85
|
+
}
|
|
4
86
|
export class ResponseHandler {
|
|
5
87
|
async handleSuccess(response, model, conversationId, streaming) {
|
|
6
88
|
if (streaming) {
|
|
@@ -8,11 +90,11 @@ export class ResponseHandler {
|
|
|
8
90
|
}
|
|
9
91
|
return this.handleNonStreaming(response, model, conversationId);
|
|
10
92
|
}
|
|
11
|
-
async handleSdkSuccess(sdkResponse, model, conversationId, streaming) {
|
|
93
|
+
async handleSdkSuccess(sdkResponse, model, conversationId, streaming, lifecycle = {}) {
|
|
12
94
|
if (streaming) {
|
|
13
|
-
return this.handleSdkStreaming(sdkResponse, model, conversationId);
|
|
95
|
+
return this.handleSdkStreaming(sdkResponse, model, conversationId, lifecycle);
|
|
14
96
|
}
|
|
15
|
-
return this.handleSdkNonStreaming(sdkResponse, model, conversationId);
|
|
97
|
+
return this.handleSdkNonStreaming(sdkResponse, model, conversationId, lifecycle.signal);
|
|
16
98
|
}
|
|
17
99
|
async handleStreaming(response, model, conversationId) {
|
|
18
100
|
const s = transformKiroStream(response, model, conversationId);
|
|
@@ -30,25 +112,145 @@ export class ResponseHandler {
|
|
|
30
112
|
}
|
|
31
113
|
}), { headers: { 'Content-Type': 'text/event-stream' } });
|
|
32
114
|
}
|
|
33
|
-
async handleSdkStreaming(sdkResponse, model, conversationId) {
|
|
34
|
-
const
|
|
115
|
+
async handleSdkStreaming(sdkResponse, model, conversationId, lifecycle) {
|
|
116
|
+
const wrapped = wrapSdkEventStream(sdkResponse, lifecycle.signal);
|
|
117
|
+
const transformed = transformSdkStream(wrapped.response, model, conversationId);
|
|
118
|
+
const buffered = [];
|
|
119
|
+
let firstSemantic;
|
|
120
|
+
while (true) {
|
|
121
|
+
const item = await transformed.next();
|
|
122
|
+
if (item.done) {
|
|
123
|
+
await lifecycle.onComplete?.();
|
|
124
|
+
lifecycle.onTerminal?.();
|
|
125
|
+
let index = 0;
|
|
126
|
+
return new Response(new ReadableStream({
|
|
127
|
+
pull(controller) {
|
|
128
|
+
const chunk = buffered[index++];
|
|
129
|
+
if (chunk)
|
|
130
|
+
controller.enqueue(chunk);
|
|
131
|
+
if (index >= buffered.length)
|
|
132
|
+
controller.close();
|
|
133
|
+
}
|
|
134
|
+
}, { highWaterMark: 0 }), { headers: { 'Content-Type': 'text/event-stream' } });
|
|
135
|
+
}
|
|
136
|
+
const encoded = encodeSseChunk(item.value);
|
|
137
|
+
if (isSemanticChunk(item.value)) {
|
|
138
|
+
firstSemantic = encoded;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
buffered.push(encoded);
|
|
142
|
+
}
|
|
143
|
+
let terminal = false;
|
|
144
|
+
let firstPull = true;
|
|
145
|
+
let abortListener;
|
|
146
|
+
const finish = () => {
|
|
147
|
+
if (terminal)
|
|
148
|
+
return;
|
|
149
|
+
terminal = true;
|
|
150
|
+
if (abortListener && lifecycle.signal) {
|
|
151
|
+
lifecycle.signal.removeEventListener('abort', abortListener);
|
|
152
|
+
}
|
|
153
|
+
lifecycle.onTerminal?.();
|
|
154
|
+
};
|
|
155
|
+
const cleanupIterators = async () => {
|
|
156
|
+
try {
|
|
157
|
+
await transformed.return(undefined);
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
await wrapped.closeRaw();
|
|
161
|
+
};
|
|
35
162
|
return new Response(new ReadableStream({
|
|
36
|
-
|
|
163
|
+
start(controller) {
|
|
164
|
+
if (!lifecycle.signal)
|
|
165
|
+
return;
|
|
166
|
+
abortListener = () => {
|
|
167
|
+
if (terminal)
|
|
168
|
+
return;
|
|
169
|
+
const reason = abortReason(lifecycle.signal);
|
|
170
|
+
void cleanupIterators();
|
|
171
|
+
finish();
|
|
172
|
+
controller.error(reason);
|
|
173
|
+
};
|
|
174
|
+
if (lifecycle.signal.aborted)
|
|
175
|
+
abortListener();
|
|
176
|
+
else
|
|
177
|
+
lifecycle.signal.addEventListener('abort', abortListener, { once: true });
|
|
178
|
+
},
|
|
179
|
+
async pull(controller) {
|
|
180
|
+
if (terminal)
|
|
181
|
+
return;
|
|
182
|
+
if (firstPull) {
|
|
183
|
+
firstPull = false;
|
|
184
|
+
for (const chunk of buffered)
|
|
185
|
+
controller.enqueue(chunk);
|
|
186
|
+
controller.enqueue(firstSemantic);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
37
189
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
190
|
+
const item = await transformed.next();
|
|
191
|
+
if (item.done) {
|
|
192
|
+
await lifecycle.onComplete?.();
|
|
193
|
+
finish();
|
|
194
|
+
controller.close();
|
|
195
|
+
return;
|
|
40
196
|
}
|
|
41
|
-
|
|
197
|
+
controller.enqueue(encodeSseChunk(item.value));
|
|
42
198
|
}
|
|
43
|
-
catch (
|
|
44
|
-
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (terminal)
|
|
201
|
+
return;
|
|
202
|
+
const mapped = error instanceof SdkEventStreamIterationError
|
|
203
|
+
? (lifecycle.mapError?.(error, true) ?? error)
|
|
204
|
+
: error;
|
|
205
|
+
finish();
|
|
206
|
+
controller.error(mapped);
|
|
45
207
|
}
|
|
208
|
+
},
|
|
209
|
+
async cancel(reason) {
|
|
210
|
+
if (terminal)
|
|
211
|
+
return;
|
|
212
|
+
finish();
|
|
213
|
+
lifecycle.onCancel?.(reason);
|
|
214
|
+
await cleanupIterators();
|
|
46
215
|
}
|
|
47
|
-
}), { headers: { 'Content-Type': 'text/event-stream' } });
|
|
216
|
+
}, { highWaterMark: 0 }), { headers: { 'Content-Type': 'text/event-stream' } });
|
|
48
217
|
}
|
|
49
|
-
async
|
|
50
|
-
|
|
51
|
-
|
|
218
|
+
async handleSdkNonStreaming(sdkResponse, model, conversationId, signal) {
|
|
219
|
+
// For non-streaming SDK responses, collect all events
|
|
220
|
+
let content = '';
|
|
221
|
+
let reasoningContent = '';
|
|
222
|
+
const toolCalls = [];
|
|
223
|
+
let inputTokens = 0;
|
|
224
|
+
let outputTokens = 0;
|
|
225
|
+
const wrapped = wrapSdkEventStream(sdkResponse, signal);
|
|
226
|
+
const eventStream = wrapped.response.generateAssistantResponseResponse;
|
|
227
|
+
if (eventStream) {
|
|
228
|
+
try {
|
|
229
|
+
for await (const event of eventStream) {
|
|
230
|
+
if (event.reasoningContentEvent?.text) {
|
|
231
|
+
reasoningContent += event.reasoningContentEvent.text;
|
|
232
|
+
}
|
|
233
|
+
if (event.assistantResponseEvent?.content) {
|
|
234
|
+
content += event.assistantResponseEvent.content;
|
|
235
|
+
}
|
|
236
|
+
if (event.toolUseEvent) {
|
|
237
|
+
toolCalls.push(event.toolUseEvent);
|
|
238
|
+
}
|
|
239
|
+
if (event.metadataEvent?.tokenUsage) {
|
|
240
|
+
inputTokens = event.metadataEvent.tokenUsage.inputTokens || 0;
|
|
241
|
+
outputTokens = event.metadataEvent.tokenUsage.outputTokens || 0;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
if (signal?.aborted)
|
|
247
|
+
await wrapped.closeRaw();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const message = { role: 'assistant', content };
|
|
251
|
+
if (reasoningContent) {
|
|
252
|
+
message.reasoning_content = reasoningContent;
|
|
253
|
+
}
|
|
52
254
|
const oai = {
|
|
53
255
|
id: conversationId,
|
|
54
256
|
object: 'chat.completion',
|
|
@@ -57,18 +259,18 @@ export class ResponseHandler {
|
|
|
57
259
|
choices: [
|
|
58
260
|
{
|
|
59
261
|
index: 0,
|
|
60
|
-
message
|
|
61
|
-
finish_reason:
|
|
262
|
+
message,
|
|
263
|
+
finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop'
|
|
62
264
|
}
|
|
63
265
|
],
|
|
64
266
|
usage: {
|
|
65
|
-
prompt_tokens:
|
|
66
|
-
completion_tokens:
|
|
67
|
-
total_tokens:
|
|
267
|
+
prompt_tokens: inputTokens,
|
|
268
|
+
completion_tokens: outputTokens,
|
|
269
|
+
total_tokens: inputTokens + outputTokens
|
|
68
270
|
}
|
|
69
271
|
};
|
|
70
|
-
if (
|
|
71
|
-
oai.choices[0].message.tool_calls =
|
|
272
|
+
if (toolCalls.length > 0) {
|
|
273
|
+
oai.choices[0].message.tool_calls = toolCalls.map((tc) => ({
|
|
72
274
|
id: tc.toolUseId,
|
|
73
275
|
type: 'function',
|
|
74
276
|
function: {
|
|
@@ -81,35 +283,9 @@ export class ResponseHandler {
|
|
|
81
283
|
headers: { 'Content-Type': 'application/json' }
|
|
82
284
|
});
|
|
83
285
|
}
|
|
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
|
-
}
|
|
286
|
+
async handleNonStreaming(response, model, conversationId) {
|
|
287
|
+
const text = await response.text();
|
|
288
|
+
const p = parseEventStream(text, model);
|
|
113
289
|
const oai = {
|
|
114
290
|
id: conversationId,
|
|
115
291
|
object: 'chat.completion',
|
|
@@ -118,18 +294,18 @@ export class ResponseHandler {
|
|
|
118
294
|
choices: [
|
|
119
295
|
{
|
|
120
296
|
index: 0,
|
|
121
|
-
message,
|
|
122
|
-
finish_reason:
|
|
297
|
+
message: { role: 'assistant', content: p.content },
|
|
298
|
+
finish_reason: p.stopReason === 'tool_use' ? 'tool_calls' : 'stop'
|
|
123
299
|
}
|
|
124
300
|
],
|
|
125
301
|
usage: {
|
|
126
|
-
prompt_tokens: inputTokens,
|
|
127
|
-
completion_tokens: outputTokens,
|
|
128
|
-
total_tokens: inputTokens + outputTokens
|
|
302
|
+
prompt_tokens: p.inputTokens || 0,
|
|
303
|
+
completion_tokens: p.outputTokens || 0,
|
|
304
|
+
total_tokens: (p.inputTokens || 0) + (p.outputTokens || 0)
|
|
129
305
|
}
|
|
130
306
|
};
|
|
131
|
-
if (toolCalls.length > 0) {
|
|
132
|
-
oai.choices[0].message.tool_calls = toolCalls.map((tc) => ({
|
|
307
|
+
if (p.toolCalls.length > 0) {
|
|
308
|
+
oai.choices[0].message.tool_calls = p.toolCalls.map((tc) => ({
|
|
133
309
|
id: tc.toolUseId,
|
|
134
310
|
type: 'function',
|
|
135
311
|
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))
|