@sunerpy/opencode-kiro-auth 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/infrastructure/transformers/tool-call-parser.d.ts +15 -0
- package/dist/infrastructure/transformers/tool-call-parser.js +242 -0
- package/dist/plugin/accounts.d.ts +12 -2
- package/dist/plugin/accounts.js +26 -9
- package/dist/plugin/config/loader.js +2 -0
- package/dist/plugin/config/schema.d.ts +17 -0
- package/dist/plugin/config/schema.js +15 -0
- package/dist/plugin/request.js +26 -7
- package/dist/plugin/streaming/dialect-gate.d.ts +29 -0
- package/dist/plugin/streaming/dialect-gate.js +92 -0
- package/dist/plugin/streaming/sdk-stream-transformer.js +28 -8
- package/dist/plugin.js +4 -1
- package/package.json +1 -1
|
@@ -2,3 +2,18 @@ import type { ToolCall } from '../../plugin/types.js';
|
|
|
2
2
|
export declare function parseBracketToolCalls(text: string): ToolCall[];
|
|
3
3
|
export declare function deduplicateToolCalls(toolCalls: ToolCall[]): ToolCall[];
|
|
4
4
|
export declare function cleanToolCallsFromText(text: string, toolCalls: ToolCall[]): string;
|
|
5
|
+
export declare const DSML_MARKER = "<\uFF5CDSML\uFF5Cfunction_calls";
|
|
6
|
+
/**
|
|
7
|
+
* Recognize Anthropic XML, deepseek DSML, and legacy bracket tool-call dialects
|
|
8
|
+
* in `text`. Returns the parsed tool calls plus `cleanedText` with EXACTLY the
|
|
9
|
+
* matched dialect spans removed (all other text preserved verbatim).
|
|
10
|
+
*
|
|
11
|
+
* Phantom-execution guards:
|
|
12
|
+
* - only COMPLETE closed tags match (an unclosed `<invoke name="x">` is text);
|
|
13
|
+
* - candidates inside fenced/inline code are skipped;
|
|
14
|
+
* - a dialect that yields no parseable call is stripped, never fabricated.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseTextToolCalls(text: string): {
|
|
17
|
+
toolCalls: ToolCall[];
|
|
18
|
+
cleanedText: string;
|
|
19
|
+
};
|
|
@@ -43,3 +43,245 @@ export function cleanToolCallsFromText(text, toolCalls) {
|
|
|
43
43
|
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
|
44
44
|
return cleaned;
|
|
45
45
|
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Text-dialect tool-call recovery (bleed-stop)
|
|
48
|
+
//
|
|
49
|
+
// Some models emit tool calls as literal TEXT dialects instead of the
|
|
50
|
+
// structured toolUseEvent path:
|
|
51
|
+
// (a) Anthropic XML: <function_calls><invoke name="X"><parameter name="k">v</parameter></invoke></function_calls>
|
|
52
|
+
// (and a standalone <invoke name="X">...</invoke>)
|
|
53
|
+
// (b) deepseek DSML: <|DSML|function_calls... (U+FF5C '|', NOT ASCII '|')
|
|
54
|
+
// (c) bracket: [Called X with args:{...}] (legacy, kept)
|
|
55
|
+
//
|
|
56
|
+
// These leak verbatim and stall the turn. `parseTextToolCalls` rescues them
|
|
57
|
+
// into structured ToolCalls and returns the text with EXACTLY the matched
|
|
58
|
+
// spans removed. It is deliberately conservative: only COMPLETE closed tags
|
|
59
|
+
// match, candidates inside fenced/inline code are skipped, and a dialect that
|
|
60
|
+
// cannot be parsed is stripped (never fabricated into a phantom call).
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// deepseek DSML opening marker — the exact U+FF5C ('|') form observed leaking.
|
|
63
|
+
export const DSML_MARKER = '<\uFF5CDSML\uFF5Cfunction_calls';
|
|
64
|
+
function genToolUseId() {
|
|
65
|
+
return `tool_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Compute char ranges that are inside fenced code blocks (``` ... ```) or
|
|
69
|
+
* inline code spans (` ... `). Candidates overlapping any of these are skipped
|
|
70
|
+
* so a model *explaining* or *showing* tool-call syntax is never executed.
|
|
71
|
+
*/
|
|
72
|
+
function computeCodeRanges(text) {
|
|
73
|
+
const ranges = [];
|
|
74
|
+
const fence = /```[\s\S]*?```/g;
|
|
75
|
+
let m;
|
|
76
|
+
while ((m = fence.exec(text)) !== null) {
|
|
77
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
78
|
+
}
|
|
79
|
+
const inFence = (i) => ranges.some(([s, e]) => i >= s && i < e);
|
|
80
|
+
const inline = /`[^`\n]+`/g;
|
|
81
|
+
while ((m = inline.exec(text)) !== null) {
|
|
82
|
+
if (!inFence(m.index))
|
|
83
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
84
|
+
}
|
|
85
|
+
return ranges;
|
|
86
|
+
}
|
|
87
|
+
function overlapsCode(start, end, codeRanges) {
|
|
88
|
+
return codeRanges.some(([s, e]) => start < e && end > s);
|
|
89
|
+
}
|
|
90
|
+
function overlapsClaimed(start, end, claimed) {
|
|
91
|
+
return claimed.some(([s, e]) => start < e && end > s);
|
|
92
|
+
}
|
|
93
|
+
/** Parse `<parameter name="K">V</parameter>` pairs from an invoke body into an input object. */
|
|
94
|
+
function parseInvokeParameters(body) {
|
|
95
|
+
const input = {};
|
|
96
|
+
const paramRe = /<parameter\s+name="([^"]+)"\s*>([\s\S]*?)<\/parameter>/g;
|
|
97
|
+
let pm;
|
|
98
|
+
while ((pm = paramRe.exec(body)) !== null) {
|
|
99
|
+
const key = pm[1];
|
|
100
|
+
const rawVal = pm[2];
|
|
101
|
+
if (key === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
const val = rawVal ?? '';
|
|
104
|
+
try {
|
|
105
|
+
input[key] = JSON.parse(val);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
input[key] = val;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return input;
|
|
112
|
+
}
|
|
113
|
+
/** Build a ToolCall from a single complete `<invoke name="...">...</invoke>` block. */
|
|
114
|
+
function toolCallFromInvoke(name, body) {
|
|
115
|
+
return {
|
|
116
|
+
toolUseId: genToolUseId(),
|
|
117
|
+
name,
|
|
118
|
+
input: parseInvokeParameters(body)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Match complete Anthropic XML dialects:
|
|
123
|
+
* - `<function_calls>...(invoke)+...</function_calls>` blocks
|
|
124
|
+
* - standalone `<invoke name="X">...</invoke>` NOT inside a function_calls block
|
|
125
|
+
* Skips candidates inside code and records claimed ranges to avoid double-match.
|
|
126
|
+
*/
|
|
127
|
+
function matchAnthropicXml(text, codeRanges, claimed) {
|
|
128
|
+
const matches = [];
|
|
129
|
+
// (1) complete <function_calls>...</function_calls> blocks
|
|
130
|
+
const blockRe = /<function_calls>[\s\S]*?<\/function_calls>/g;
|
|
131
|
+
let bm;
|
|
132
|
+
while ((bm = blockRe.exec(text)) !== null) {
|
|
133
|
+
const start = bm.index;
|
|
134
|
+
const end = start + bm[0].length;
|
|
135
|
+
if (overlapsCode(start, end, codeRanges))
|
|
136
|
+
continue;
|
|
137
|
+
const invokeRe = /<invoke\s+name="([^"]+)"\s*>([\s\S]*?)<\/invoke>/g;
|
|
138
|
+
const toolCalls = [];
|
|
139
|
+
let im;
|
|
140
|
+
while ((im = invokeRe.exec(bm[0])) !== null) {
|
|
141
|
+
const name = im[1];
|
|
142
|
+
if (!name)
|
|
143
|
+
continue;
|
|
144
|
+
toolCalls.push(toolCallFromInvoke(name, im[2] ?? ''));
|
|
145
|
+
}
|
|
146
|
+
// Only treat as a tool-call span if at least one invoke parsed; otherwise
|
|
147
|
+
// it is not a real dialect payload — leave the text untouched.
|
|
148
|
+
if (toolCalls.length === 0)
|
|
149
|
+
continue;
|
|
150
|
+
matches.push({ start, end, toolCalls });
|
|
151
|
+
claimed.push([start, end]);
|
|
152
|
+
}
|
|
153
|
+
// (2) standalone complete <invoke ...>...</invoke> not inside a claimed block
|
|
154
|
+
const invokeRe = /<invoke\s+name="([^"]+)"\s*>([\s\S]*?)<\/invoke>/g;
|
|
155
|
+
let sm;
|
|
156
|
+
while ((sm = invokeRe.exec(text)) !== null) {
|
|
157
|
+
const start = sm.index;
|
|
158
|
+
const end = start + sm[0].length;
|
|
159
|
+
const name = sm[1];
|
|
160
|
+
if (!name)
|
|
161
|
+
continue;
|
|
162
|
+
if (overlapsCode(start, end, codeRanges))
|
|
163
|
+
continue;
|
|
164
|
+
if (overlapsClaimed(start, end, claimed))
|
|
165
|
+
continue;
|
|
166
|
+
matches.push({ start, end, toolCalls: [toolCallFromInvoke(name, sm[2] ?? '')] });
|
|
167
|
+
claimed.push([start, end]);
|
|
168
|
+
}
|
|
169
|
+
return matches;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Match the deepseek DSML dialect. The strip span runs from the exact U+FF5C
|
|
173
|
+
* marker to its closing counterpart if one exists, else to end-of-text (the
|
|
174
|
+
* observed trailing leak). Best-effort recovery of name/args; if not cleanly
|
|
175
|
+
* recoverable the span is stripped WITHOUT fabricating a call.
|
|
176
|
+
*/
|
|
177
|
+
function matchDsml(text, codeRanges, claimed) {
|
|
178
|
+
const matches = [];
|
|
179
|
+
let from = 0;
|
|
180
|
+
for (;;) {
|
|
181
|
+
const start = text.indexOf(DSML_MARKER, from);
|
|
182
|
+
if (start === -1)
|
|
183
|
+
break;
|
|
184
|
+
// Closing counterpart: a DSML end token (U+FF5C ... end ... U+FF5C) after
|
|
185
|
+
// the marker, else consume to end-of-text.
|
|
186
|
+
const rest = text.slice(start + DSML_MARKER.length);
|
|
187
|
+
const closeRe = /<\uFF5C[^>]*?end[^>]*?>|<\/\uFF5CDSML[^>]*?>/;
|
|
188
|
+
const cm = closeRe.exec(rest);
|
|
189
|
+
const end = cm !== null ? start + DSML_MARKER.length + cm.index + cm[0].length : text.length;
|
|
190
|
+
from = end;
|
|
191
|
+
if (overlapsCode(start, end, codeRanges))
|
|
192
|
+
continue;
|
|
193
|
+
if (overlapsClaimed(start, end, claimed))
|
|
194
|
+
continue;
|
|
195
|
+
// Best-effort recovery — only fires on a clean name + JSON args pair.
|
|
196
|
+
const span = text.slice(start, end);
|
|
197
|
+
const toolCalls = [];
|
|
198
|
+
const nameM = /name["\uFF5C=:\s]+["']?([A-Za-z0-9_]+)/.exec(span);
|
|
199
|
+
const jsonM = /(\{[\s\S]*\})/.exec(span);
|
|
200
|
+
if (nameM?.[1] && jsonM?.[1]) {
|
|
201
|
+
try {
|
|
202
|
+
toolCalls.push({
|
|
203
|
+
toolUseId: genToolUseId(),
|
|
204
|
+
name: nameM[1],
|
|
205
|
+
input: JSON.parse(jsonM[1])
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// fall through to strip-only
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
matches.push({ start, end, toolCalls });
|
|
213
|
+
claimed.push([start, end]);
|
|
214
|
+
}
|
|
215
|
+
return matches;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Match legacy `[Called X with args:{...}]` spans (for cleanedText) and their
|
|
219
|
+
* tool calls, skipping candidates inside code.
|
|
220
|
+
*/
|
|
221
|
+
function matchBracket(text, codeRanges, claimed) {
|
|
222
|
+
const matches = [];
|
|
223
|
+
const pattern = /\[Called\s+(\w+)\s+with\s+args:\s*(\{[^}]*(?:\{[^}]*\}[^}]*)*\})\]/gs;
|
|
224
|
+
let m;
|
|
225
|
+
while ((m = pattern.exec(text)) !== null) {
|
|
226
|
+
const start = m.index;
|
|
227
|
+
const end = start + m[0].length;
|
|
228
|
+
const name = m[1];
|
|
229
|
+
const argsStr = m[2];
|
|
230
|
+
if (!name || !argsStr)
|
|
231
|
+
continue;
|
|
232
|
+
if (overlapsCode(start, end, codeRanges))
|
|
233
|
+
continue;
|
|
234
|
+
if (overlapsClaimed(start, end, claimed))
|
|
235
|
+
continue;
|
|
236
|
+
let input;
|
|
237
|
+
try {
|
|
238
|
+
input = JSON.parse(argsStr);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
matches.push({
|
|
244
|
+
start,
|
|
245
|
+
end,
|
|
246
|
+
toolCalls: [{ toolUseId: genToolUseId(), name, input }]
|
|
247
|
+
});
|
|
248
|
+
claimed.push([start, end]);
|
|
249
|
+
}
|
|
250
|
+
return matches;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Recognize Anthropic XML, deepseek DSML, and legacy bracket tool-call dialects
|
|
254
|
+
* in `text`. Returns the parsed tool calls plus `cleanedText` with EXACTLY the
|
|
255
|
+
* matched dialect spans removed (all other text preserved verbatim).
|
|
256
|
+
*
|
|
257
|
+
* Phantom-execution guards:
|
|
258
|
+
* - only COMPLETE closed tags match (an unclosed `<invoke name="x">` is text);
|
|
259
|
+
* - candidates inside fenced/inline code are skipped;
|
|
260
|
+
* - a dialect that yields no parseable call is stripped, never fabricated.
|
|
261
|
+
*/
|
|
262
|
+
export function parseTextToolCalls(text) {
|
|
263
|
+
if (!text)
|
|
264
|
+
return { toolCalls: [], cleanedText: text };
|
|
265
|
+
const codeRanges = computeCodeRanges(text);
|
|
266
|
+
const claimed = [];
|
|
267
|
+
const matches = [
|
|
268
|
+
...matchAnthropicXml(text, codeRanges, claimed),
|
|
269
|
+
...matchDsml(text, codeRanges, claimed),
|
|
270
|
+
...matchBracket(text, codeRanges, claimed)
|
|
271
|
+
];
|
|
272
|
+
if (matches.length === 0)
|
|
273
|
+
return { toolCalls: [], cleanedText: text };
|
|
274
|
+
matches.sort((a, b) => a.start - b.start);
|
|
275
|
+
const toolCalls = [];
|
|
276
|
+
let cleanedText = '';
|
|
277
|
+
let cursor = 0;
|
|
278
|
+
for (const mt of matches) {
|
|
279
|
+
if (mt.start < cursor)
|
|
280
|
+
continue; // defensive against any residual overlap
|
|
281
|
+
cleanedText += text.slice(cursor, mt.start);
|
|
282
|
+
cursor = mt.end;
|
|
283
|
+
toolCalls.push(...mt.toolCalls);
|
|
284
|
+
}
|
|
285
|
+
cleanedText += text.slice(cursor);
|
|
286
|
+
return { toolCalls, cleanedText };
|
|
287
|
+
}
|
|
@@ -6,8 +6,18 @@ export declare class AccountManager {
|
|
|
6
6
|
private strategy;
|
|
7
7
|
private lastToastTime;
|
|
8
8
|
private lastUsageToastTime;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
private rrCursor;
|
|
10
|
+
private stickyId?;
|
|
11
|
+
private quotaAvoidanceEnabled;
|
|
12
|
+
private quotaReserveThreshold;
|
|
13
|
+
constructor(accounts: ManagedAccount[], strategy?: AccountSelectionStrategy, opts?: {
|
|
14
|
+
quotaAvoidanceEnabled?: boolean;
|
|
15
|
+
quotaReserveThreshold?: number;
|
|
16
|
+
});
|
|
17
|
+
static loadFromDisk(strategy?: AccountSelectionStrategy, opts?: {
|
|
18
|
+
quotaAvoidanceEnabled?: boolean;
|
|
19
|
+
quotaReserveThreshold?: number;
|
|
20
|
+
}): Promise<AccountManager>;
|
|
11
21
|
getAccountCount(): number;
|
|
12
22
|
getAccounts(): ManagedAccount[];
|
|
13
23
|
shouldShowToast(debounce?: number): boolean;
|
package/dist/plugin/accounts.js
CHANGED
|
@@ -15,12 +15,18 @@ export class AccountManager {
|
|
|
15
15
|
strategy;
|
|
16
16
|
lastToastTime = 0;
|
|
17
17
|
lastUsageToastTime = 0;
|
|
18
|
-
|
|
18
|
+
rrCursor = 0;
|
|
19
|
+
stickyId;
|
|
20
|
+
quotaAvoidanceEnabled;
|
|
21
|
+
quotaReserveThreshold;
|
|
22
|
+
constructor(accounts, strategy = 'sticky', opts) {
|
|
19
23
|
this.accounts = accounts;
|
|
20
24
|
this.cursor = 0;
|
|
21
25
|
this.strategy = strategy;
|
|
26
|
+
this.quotaAvoidanceEnabled = opts?.quotaAvoidanceEnabled ?? true;
|
|
27
|
+
this.quotaReserveThreshold = opts?.quotaReserveThreshold ?? 0.95;
|
|
22
28
|
}
|
|
23
|
-
static async loadFromDisk(strategy) {
|
|
29
|
+
static async loadFromDisk(strategy, opts) {
|
|
24
30
|
const rows = kiroDb.getAccounts();
|
|
25
31
|
const accounts = rows.map((r) => ({
|
|
26
32
|
id: r.id,
|
|
@@ -44,7 +50,7 @@ export class AccountManager {
|
|
|
44
50
|
usedCount: r.used_count,
|
|
45
51
|
limitCount: r.limit_count
|
|
46
52
|
}));
|
|
47
|
-
return new AccountManager(accounts, strategy || 'sticky');
|
|
53
|
+
return new AccountManager(accounts, strategy || 'sticky', opts);
|
|
48
54
|
}
|
|
49
55
|
getAccountCount() {
|
|
50
56
|
return this.accounts.length;
|
|
@@ -86,17 +92,29 @@ export class AccountManager {
|
|
|
86
92
|
}
|
|
87
93
|
return !(a.rateLimitResetTime && now < a.rateLimitResetTime);
|
|
88
94
|
});
|
|
95
|
+
let candidatePool = available;
|
|
96
|
+
if (this.accounts.length > 1 && this.quotaAvoidanceEnabled) {
|
|
97
|
+
const ratio = (a) => a.limitCount && a.limitCount > 0 ? (a.usedCount || 0) / a.limitCount : 0;
|
|
98
|
+
const ample = available.filter((a) => ratio(a) < this.quotaReserveThreshold);
|
|
99
|
+
// used>=limit stays in nearFull (soft/drainable, NOT hard-excluded): the
|
|
100
|
+
// real 402 is the authoritative exhaustion signal and already
|
|
101
|
+
// hard-switches accounts in error-handler.
|
|
102
|
+
const nearFull = available.filter((a) => ratio(a) >= this.quotaReserveThreshold);
|
|
103
|
+
candidatePool = ample.length > 0 ? ample : nearFull;
|
|
104
|
+
}
|
|
89
105
|
let selected;
|
|
90
|
-
if (
|
|
106
|
+
if (candidatePool.length > 0) {
|
|
91
107
|
if (this.strategy === 'sticky') {
|
|
92
|
-
selected =
|
|
108
|
+
selected = candidatePool.find((a) => a.id === this.stickyId) || candidatePool[0];
|
|
109
|
+
if (selected)
|
|
110
|
+
this.stickyId = selected.id;
|
|
93
111
|
}
|
|
94
112
|
else if (this.strategy === 'round-robin') {
|
|
95
|
-
selected =
|
|
96
|
-
this.
|
|
113
|
+
selected = candidatePool[this.rrCursor % candidatePool.length];
|
|
114
|
+
this.rrCursor++;
|
|
97
115
|
}
|
|
98
116
|
else if (this.strategy === 'lowest-usage') {
|
|
99
|
-
selected = [...
|
|
117
|
+
selected = [...candidatePool].sort((a, b) => (a.usedCount || 0) - (b.usedCount || 0) || (a.lastUsed || 0) - (b.lastUsed || 0))[0];
|
|
100
118
|
}
|
|
101
119
|
}
|
|
102
120
|
if (!selected) {
|
|
@@ -113,7 +131,6 @@ export class AccountManager {
|
|
|
113
131
|
if (selected) {
|
|
114
132
|
selected.lastUsed = now;
|
|
115
133
|
selected.usedCount = (selected.usedCount || 0) + 1;
|
|
116
|
-
this.cursor = this.accounts.indexOf(selected);
|
|
117
134
|
return selected;
|
|
118
135
|
}
|
|
119
136
|
return null;
|
|
@@ -90,6 +90,8 @@ function applyEnvOverrides(config) {
|
|
|
90
90
|
account_selection_strategy: env.KIRO_ACCOUNT_SELECTION_STRATEGY
|
|
91
91
|
? AccountSelectionStrategySchema.catch('lowest-usage').parse(env.KIRO_ACCOUNT_SELECTION_STRATEGY)
|
|
92
92
|
: config.account_selection_strategy,
|
|
93
|
+
quota_avoidance_enabled: parseBooleanEnv(env.KIRO_QUOTA_AVOIDANCE_ENABLED, config.quota_avoidance_enabled),
|
|
94
|
+
quota_reserve_threshold: parseNumberEnv(env.KIRO_QUOTA_RESERVE_THRESHOLD, config.quota_reserve_threshold),
|
|
93
95
|
default_region: env.KIRO_DEFAULT_REGION
|
|
94
96
|
? RegionSchema.catch('us-east-1').parse(env.KIRO_DEFAULT_REGION)
|
|
95
97
|
: config.default_region,
|
|
@@ -19,6 +19,19 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
19
19
|
idc_region: z.ZodOptional<z.ZodEnum<["us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-2", "ap-southeast-3", "ap-southeast-5", "ap-southeast-4", "ap-south-1", "ap-southeast-6", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-east-2", "ap-southeast-7", "ap-northeast-1", "ca-central-1", "ca-west-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-south-2", "eu-north-1", "eu-central-2", "il-central-1", "mx-central-1", "me-south-1", "me-central-1", "sa-east-1"]>>;
|
|
20
20
|
idc_profile_arn: z.ZodOptional<z.ZodString>;
|
|
21
21
|
account_selection_strategy: z.ZodDefault<z.ZodEnum<["sticky", "round-robin", "lowest-usage"]>>;
|
|
22
|
+
/**
|
|
23
|
+
* Softly avoid accounts whose usage ratio is at/above
|
|
24
|
+
* `quota_reserve_threshold` when other accounts still have room. When ALL
|
|
25
|
+
* healthy accounts are near-full they are drained anyway (the real 402 in
|
|
26
|
+
* error-handler is the authoritative hard-switch). Only affects
|
|
27
|
+
* multi-account selection; single-account behavior is unchanged.
|
|
28
|
+
*/
|
|
29
|
+
quota_avoidance_enabled: z.ZodDefault<z.ZodBoolean>;
|
|
30
|
+
/**
|
|
31
|
+
* Usage ratio (used/limit) at/above which an account is considered
|
|
32
|
+
* near-full and softly avoided. Default 0.95 (95%).
|
|
33
|
+
*/
|
|
34
|
+
quota_reserve_threshold: z.ZodDefault<z.ZodNumber>;
|
|
22
35
|
default_region: z.ZodDefault<z.ZodEnum<["us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-2", "ap-southeast-3", "ap-southeast-5", "ap-southeast-4", "ap-south-1", "ap-southeast-6", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-east-2", "ap-southeast-7", "ap-northeast-1", "ca-central-1", "ca-west-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-south-2", "eu-north-1", "eu-central-2", "il-central-1", "mx-central-1", "me-south-1", "me-central-1", "sa-east-1"]>>;
|
|
23
36
|
rate_limit_retry_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
24
37
|
rate_limit_max_retries: z.ZodDefault<z.ZodNumber>;
|
|
@@ -52,6 +65,8 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
52
65
|
auto_effort_mapping: z.ZodDefault<z.ZodBoolean>;
|
|
53
66
|
}, "strip", z.ZodTypeAny, {
|
|
54
67
|
account_selection_strategy: "sticky" | "round-robin" | "lowest-usage";
|
|
68
|
+
quota_avoidance_enabled: boolean;
|
|
69
|
+
quota_reserve_threshold: number;
|
|
55
70
|
default_region: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1";
|
|
56
71
|
rate_limit_retry_delay_ms: number;
|
|
57
72
|
rate_limit_max_retries: number;
|
|
@@ -77,6 +92,8 @@ export declare const KiroConfigSchema: z.ZodObject<{
|
|
|
77
92
|
idc_region?: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1" | undefined;
|
|
78
93
|
idc_profile_arn?: string | undefined;
|
|
79
94
|
account_selection_strategy?: "sticky" | "round-robin" | "lowest-usage" | undefined;
|
|
95
|
+
quota_avoidance_enabled?: boolean | undefined;
|
|
96
|
+
quota_reserve_threshold?: number | undefined;
|
|
80
97
|
default_region?: "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2" | "af-south-1" | "ap-east-1" | "ap-south-2" | "ap-southeast-3" | "ap-southeast-5" | "ap-southeast-4" | "ap-south-1" | "ap-southeast-6" | "ap-northeast-3" | "ap-northeast-2" | "ap-southeast-1" | "ap-southeast-2" | "ap-east-2" | "ap-southeast-7" | "ap-northeast-1" | "ca-central-1" | "ca-west-1" | "eu-central-1" | "eu-west-1" | "eu-west-2" | "eu-south-1" | "eu-west-3" | "eu-south-2" | "eu-north-1" | "eu-central-2" | "il-central-1" | "mx-central-1" | "me-south-1" | "me-central-1" | "sa-east-1" | undefined;
|
|
81
98
|
rate_limit_retry_delay_ms?: number | undefined;
|
|
82
99
|
rate_limit_max_retries?: number | undefined;
|
|
@@ -51,6 +51,19 @@ export const KiroConfigSchema = z.object({
|
|
|
51
51
|
idc_region: RegionSchema.optional(),
|
|
52
52
|
idc_profile_arn: z.string().optional(),
|
|
53
53
|
account_selection_strategy: AccountSelectionStrategySchema.default('lowest-usage'),
|
|
54
|
+
/**
|
|
55
|
+
* Softly avoid accounts whose usage ratio is at/above
|
|
56
|
+
* `quota_reserve_threshold` when other accounts still have room. When ALL
|
|
57
|
+
* healthy accounts are near-full they are drained anyway (the real 402 in
|
|
58
|
+
* error-handler is the authoritative hard-switch). Only affects
|
|
59
|
+
* multi-account selection; single-account behavior is unchanged.
|
|
60
|
+
*/
|
|
61
|
+
quota_avoidance_enabled: z.boolean().default(true),
|
|
62
|
+
/**
|
|
63
|
+
* Usage ratio (used/limit) at/above which an account is considered
|
|
64
|
+
* near-full and softly avoided. Default 0.95 (95%).
|
|
65
|
+
*/
|
|
66
|
+
quota_reserve_threshold: z.number().min(0).max(1).default(0.95),
|
|
54
67
|
default_region: RegionSchema.default('us-east-1'),
|
|
55
68
|
rate_limit_retry_delay_ms: z.number().min(1000).max(60000).default(5000),
|
|
56
69
|
rate_limit_max_retries: z.number().min(0).max(10).default(3),
|
|
@@ -85,6 +98,8 @@ export const KiroConfigSchema = z.object({
|
|
|
85
98
|
});
|
|
86
99
|
export const DEFAULT_CONFIG = {
|
|
87
100
|
account_selection_strategy: 'lowest-usage',
|
|
101
|
+
quota_avoidance_enabled: true,
|
|
102
|
+
quota_reserve_threshold: 0.95,
|
|
88
103
|
default_region: 'us-east-1',
|
|
89
104
|
rate_limit_retry_delay_ms: 5000,
|
|
90
105
|
rate_limit_max_retries: 3,
|
package/dist/plugin/request.js
CHANGED
|
@@ -7,6 +7,28 @@ import { convertToolsToCodeWhisperer, deduplicateToolResults } from '../infrastr
|
|
|
7
7
|
import { getEffectiveEffort, resolveEffort } from './effort.js';
|
|
8
8
|
import { convertImagesToKiroFormat, extractAllImages, extractTextFromParts } from './image-handler.js';
|
|
9
9
|
import { resolveModelVariant } from './models.js';
|
|
10
|
+
function jsonSchemaTypeOf(value) {
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return 'array';
|
|
13
|
+
if (value === null)
|
|
14
|
+
return 'string';
|
|
15
|
+
const t = typeof value;
|
|
16
|
+
if (t === 'number' || t === 'boolean' || t === 'string' || t === 'object')
|
|
17
|
+
return t;
|
|
18
|
+
return 'string';
|
|
19
|
+
}
|
|
20
|
+
// No `required` fields are inferred: fabricated required keys risk a 400.
|
|
21
|
+
function inferToolSpecFromHistory(name, toolUsesInHistory) {
|
|
22
|
+
const sample = toolUsesInHistory.find((tu) => tu.name === name && tu.input && typeof tu.input === 'object' && !Array.isArray(tu.input));
|
|
23
|
+
const properties = {};
|
|
24
|
+
if (sample && sample.input) {
|
|
25
|
+
for (const [key, val] of Object.entries(sample.input)) {
|
|
26
|
+
properties[key] = { type: jsonSchemaTypeOf(val) };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const json = Object.keys(properties).length > 0 ? { type: 'object', properties } : { type: 'object' };
|
|
30
|
+
return { name, description: `Tool ${name}`, inputSchema: { json } };
|
|
31
|
+
}
|
|
10
32
|
function buildCodeWhispererRequest(body, model, auth, think = false, budget = 20000, showToast) {
|
|
11
33
|
const req = typeof body === 'string' ? JSON.parse(body) : body;
|
|
12
34
|
const { messages, tools, system } = req;
|
|
@@ -228,13 +250,10 @@ function buildCodeWhispererRequest(body, model, auth, think = false, budget = 20
|
|
|
228
250
|
const existingToolNames = new Set(existingTools.map((t) => t.toolSpecification?.name).filter(Boolean));
|
|
229
251
|
const missingToolNames = Array.from(toolNamesInHistory).filter((name) => !existingToolNames.has(name));
|
|
230
252
|
if (missingToolNames.length > 0) {
|
|
231
|
-
const placeholderTools = missingToolNames.map((name) =>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
inputSchema: { json: { type: 'object', properties: {} } }
|
|
236
|
-
}
|
|
237
|
-
}));
|
|
253
|
+
const placeholderTools = missingToolNames.map((name) => {
|
|
254
|
+
const inferred = inferToolSpecFromHistory(name, toolUsesInHistory);
|
|
255
|
+
return { toolSpecification: inferred };
|
|
256
|
+
});
|
|
238
257
|
if (!uim.userInputMessageContext)
|
|
239
258
|
uim.userInputMessageContext = {};
|
|
240
259
|
uim.userInputMessageContext.tools = [...existingTools, ...placeholderTools];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ToolCall } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Streaming suppression gate for text-dialect tool calls.
|
|
4
|
+
*
|
|
5
|
+
* Visible assistant reply text is pushed through the gate as it streams. While
|
|
6
|
+
* no dialect opening marker has appeared, the gate returns the safe prefix to
|
|
7
|
+
* stream (holding back only a possible partial-marker tail). Once a marker
|
|
8
|
+
* appears, everything from the marker onward is withheld. At finalization,
|
|
9
|
+
* `finalize()` parses the full accumulated text into structured tool calls and
|
|
10
|
+
* returns the remaining non-dialect text (dialect spans removed) that still
|
|
11
|
+
* needs to be emitted.
|
|
12
|
+
*/
|
|
13
|
+
export declare class DialectGate {
|
|
14
|
+
private accumulated;
|
|
15
|
+
private emitted;
|
|
16
|
+
private markerSeen;
|
|
17
|
+
/** Append a visible-text chunk; returns the substring safe to emit now. */
|
|
18
|
+
push(text: string): string;
|
|
19
|
+
/** True once a dialect opening marker has been observed (streaming suppressed). */
|
|
20
|
+
get suppressing(): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Finalize: parse the full accumulated text into structured tool calls and
|
|
23
|
+
* return the non-dialect text that was buffered but not yet emitted.
|
|
24
|
+
*/
|
|
25
|
+
finalize(): {
|
|
26
|
+
toolCalls: ToolCall[];
|
|
27
|
+
remainderText: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { DSML_MARKER, parseTextToolCalls } from '../../infrastructure/transformers/tool-call-parser.js';
|
|
2
|
+
// Opening markers that signal a text-dialect tool call may be starting. Once
|
|
3
|
+
// any of these appears in the accumulated visible text, we stop streaming
|
|
4
|
+
// further visible text (buffer it) so a dialect span is never emitted as
|
|
5
|
+
// visible `delta.content`. Authoritative parsing happens only at finalization
|
|
6
|
+
// on the FULL accumulated text (never per-fragment).
|
|
7
|
+
const OPENING_MARKERS = ['<function_calls', '<invoke name=', DSML_MARKER];
|
|
8
|
+
const MAX_MARKER_LEN = Math.max(...OPENING_MARKERS.map((m) => m.length));
|
|
9
|
+
/** Earliest index of any opening marker in `text`, or -1. */
|
|
10
|
+
function firstMarkerIndex(text) {
|
|
11
|
+
let earliest = -1;
|
|
12
|
+
for (const marker of OPENING_MARKERS) {
|
|
13
|
+
const idx = text.indexOf(marker);
|
|
14
|
+
if (idx !== -1 && (earliest === -1 || idx < earliest))
|
|
15
|
+
earliest = idx;
|
|
16
|
+
}
|
|
17
|
+
return earliest;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Length of the longest suffix of `text` that is a proper prefix of some
|
|
21
|
+
* opening marker — i.e. the tail might be the start of a marker split across
|
|
22
|
+
* chunks. That tail is reserved (not emitted yet) to avoid streaming half a
|
|
23
|
+
* marker as visible text.
|
|
24
|
+
*/
|
|
25
|
+
function partialMarkerTail(text) {
|
|
26
|
+
const maxLook = Math.min(text.length, MAX_MARKER_LEN - 1);
|
|
27
|
+
for (let len = maxLook; len > 0; len--) {
|
|
28
|
+
const tail = text.slice(text.length - len);
|
|
29
|
+
for (const marker of OPENING_MARKERS) {
|
|
30
|
+
if (marker.length > len && marker.startsWith(tail))
|
|
31
|
+
return len;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Streaming suppression gate for text-dialect tool calls.
|
|
38
|
+
*
|
|
39
|
+
* Visible assistant reply text is pushed through the gate as it streams. While
|
|
40
|
+
* no dialect opening marker has appeared, the gate returns the safe prefix to
|
|
41
|
+
* stream (holding back only a possible partial-marker tail). Once a marker
|
|
42
|
+
* appears, everything from the marker onward is withheld. At finalization,
|
|
43
|
+
* `finalize()` parses the full accumulated text into structured tool calls and
|
|
44
|
+
* returns the remaining non-dialect text (dialect spans removed) that still
|
|
45
|
+
* needs to be emitted.
|
|
46
|
+
*/
|
|
47
|
+
export class DialectGate {
|
|
48
|
+
accumulated = '';
|
|
49
|
+
emitted = 0;
|
|
50
|
+
markerSeen = false;
|
|
51
|
+
/** Append a visible-text chunk; returns the substring safe to emit now. */
|
|
52
|
+
push(text) {
|
|
53
|
+
if (!text)
|
|
54
|
+
return '';
|
|
55
|
+
this.accumulated += text;
|
|
56
|
+
if (!this.markerSeen) {
|
|
57
|
+
const markerIdx = firstMarkerIndex(this.accumulated);
|
|
58
|
+
if (markerIdx !== -1)
|
|
59
|
+
this.markerSeen = true;
|
|
60
|
+
}
|
|
61
|
+
let safeEnd;
|
|
62
|
+
if (this.markerSeen) {
|
|
63
|
+
safeEnd = firstMarkerIndex(this.accumulated);
|
|
64
|
+
if (safeEnd === -1)
|
|
65
|
+
safeEnd = this.accumulated.length;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// No marker yet — but the tail could be the start of one; reserve it.
|
|
69
|
+
safeEnd = this.accumulated.length - partialMarkerTail(this.accumulated);
|
|
70
|
+
}
|
|
71
|
+
if (safeEnd <= this.emitted)
|
|
72
|
+
return '';
|
|
73
|
+
const out = this.accumulated.slice(this.emitted, safeEnd);
|
|
74
|
+
this.emitted = safeEnd;
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/** True once a dialect opening marker has been observed (streaming suppressed). */
|
|
78
|
+
get suppressing() {
|
|
79
|
+
return this.markerSeen;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Finalize: parse the full accumulated text into structured tool calls and
|
|
83
|
+
* return the non-dialect text that was buffered but not yet emitted.
|
|
84
|
+
*/
|
|
85
|
+
finalize() {
|
|
86
|
+
const { toolCalls, cleanedText } = parseTextToolCalls(this.accumulated);
|
|
87
|
+
// Text before the first marker was already emitted verbatim and is never
|
|
88
|
+
// part of a dialect span, so cleanedText[0..emitted) == what we emitted.
|
|
89
|
+
const remainderText = cleanedText.length > this.emitted ? cleanedText.slice(this.emitted) : '';
|
|
90
|
+
return { toolCalls, remainderText };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { parseBracketToolCalls } from '../../infrastructure/transformers/tool-call-parser.js';
|
|
2
1
|
import { getContextWindowSize } from '../models.js';
|
|
3
2
|
import { estimateTokens } from '../response.js';
|
|
3
|
+
import { DialectGate } from './dialect-gate.js';
|
|
4
4
|
import { convertToOpenAI } from './openai-converter.js';
|
|
5
5
|
import { findRealTag } from './stream-parser.js';
|
|
6
6
|
import { createTextDeltaEvents, createThinkingDeltaEvents, stopBlock } from './stream-state.js';
|
|
@@ -24,6 +24,19 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
|
|
|
24
24
|
let contextUsagePercentage = null;
|
|
25
25
|
const toolCalls = [];
|
|
26
26
|
let currentToolCall = null;
|
|
27
|
+
// Text deltas route through the gate so a dialect span is withheld from the
|
|
28
|
+
// visible stream once its opening marker appears (recovered at finalization).
|
|
29
|
+
const dialectGate = new DialectGate();
|
|
30
|
+
const toChunk = (ev) => {
|
|
31
|
+
if (ev.type === 'content_block_delta' && ev.delta?.type === 'text_delta') {
|
|
32
|
+
const safe = dialectGate.push(ev.delta.text ?? '');
|
|
33
|
+
if (!safe)
|
|
34
|
+
return null;
|
|
35
|
+
const gated = { ...ev, delta: { ...ev.delta, text: safe } };
|
|
36
|
+
return convertToOpenAI(gated, conversationId, model);
|
|
37
|
+
}
|
|
38
|
+
return convertToOpenAI(ev, conversationId, model);
|
|
39
|
+
};
|
|
27
40
|
// Probe (opus-4.8): reasoning streams via `reasoningContentEvent{text,signature}` as a
|
|
28
41
|
// contiguous run BEFORE `assistantResponseEvent.content`; no `<thinking>` tags emitted.
|
|
29
42
|
// signature is metadata and ignored for rendering.
|
|
@@ -65,7 +78,7 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
|
|
|
65
78
|
}
|
|
66
79
|
if (reasoningStarted) {
|
|
67
80
|
for (const ev of createTextDeltaEvents(text, streamState)) {
|
|
68
|
-
const _c =
|
|
81
|
+
const _c = toChunk(ev);
|
|
69
82
|
if (_c !== null)
|
|
70
83
|
yield _c;
|
|
71
84
|
}
|
|
@@ -74,7 +87,7 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
|
|
|
74
87
|
if (!thinkingRequested) {
|
|
75
88
|
for (const ev of createTextDeltaEvents(text, streamState)) {
|
|
76
89
|
{
|
|
77
|
-
const _c =
|
|
90
|
+
const _c = toChunk(ev);
|
|
78
91
|
if (_c !== null)
|
|
79
92
|
yield _c;
|
|
80
93
|
}
|
|
@@ -142,7 +155,7 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
|
|
|
142
155
|
}
|
|
143
156
|
}
|
|
144
157
|
for (const ev of deltaEvents) {
|
|
145
|
-
const chunk =
|
|
158
|
+
const chunk = toChunk(ev);
|
|
146
159
|
if (chunk !== null)
|
|
147
160
|
yield chunk;
|
|
148
161
|
}
|
|
@@ -218,21 +231,28 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
|
|
|
218
231
|
}
|
|
219
232
|
else {
|
|
220
233
|
for (const ev of createTextDeltaEvents(streamState.buffer, streamState)) {
|
|
221
|
-
const _c =
|
|
234
|
+
const _c = toChunk(ev);
|
|
222
235
|
if (_c !== null)
|
|
223
236
|
yield _c;
|
|
224
237
|
}
|
|
225
238
|
streamState.buffer = '';
|
|
226
239
|
}
|
|
227
240
|
}
|
|
241
|
+
const { toolCalls: dialectToolCalls, remainderText } = dialectGate.finalize();
|
|
242
|
+
if (remainderText) {
|
|
243
|
+
for (const ev of createTextDeltaEvents(remainderText, streamState)) {
|
|
244
|
+
const _c = convertToOpenAI(ev, conversationId, model);
|
|
245
|
+
if (_c !== null)
|
|
246
|
+
yield _c;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
228
249
|
for (const ev of stopBlock(streamState.textBlockIndex, streamState)) {
|
|
229
250
|
const _c = convertToOpenAI(ev, conversationId, model);
|
|
230
251
|
if (_c !== null)
|
|
231
252
|
yield _c;
|
|
232
253
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
for (const btc of bracketToolCalls) {
|
|
254
|
+
if (dialectToolCalls.length > 0) {
|
|
255
|
+
for (const btc of dialectToolCalls) {
|
|
236
256
|
toolCalls.push({
|
|
237
257
|
toolUseId: btc.toolUseId,
|
|
238
258
|
name: btc.name,
|
package/dist/plugin.js
CHANGED
|
@@ -15,7 +15,10 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
15
15
|
const cache = new AccountCache(60000);
|
|
16
16
|
const repository = new AccountRepository(cache);
|
|
17
17
|
const authHandler = new AuthHandler(config, repository);
|
|
18
|
-
const accountManager = await AccountManager.loadFromDisk(config.account_selection_strategy
|
|
18
|
+
const accountManager = await AccountManager.loadFromDisk(config.account_selection_strategy, {
|
|
19
|
+
quotaAvoidanceEnabled: config.quota_avoidance_enabled,
|
|
20
|
+
quotaReserveThreshold: config.quota_reserve_threshold
|
|
21
|
+
});
|
|
19
22
|
authHandler.setAccountManager(accountManager);
|
|
20
23
|
const requestHandler = new RequestHandler(accountManager, config, repository, client);
|
|
21
24
|
// Compute the base URL once so both the config hook and auth loader use the same value
|