@yeaft/webchat-agent 1.0.578 → 1.0.580
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +97 -97
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/config.js +12 -5
- package/yeaft/engine.js +8 -4
- package/yeaft/llm/adapter.js +23 -0
- package/yeaft/llm/anthropic.js +93 -44
- package/yeaft/llm/known-providers.js +18 -1
- package/yeaft/llm/openai-responses.js +10 -8
- package/yeaft/llm/provider-state.js +1 -0
- package/yeaft/llm/router.js +18 -9
- package/yeaft/llm/usage-accounting.js +2 -2
- package/yeaft/models.js +7 -1
- package/yeaft/web-bridge.js +24 -4
|
Binary file
|
package/package.json
CHANGED
package/yeaft/config.js
CHANGED
|
@@ -84,16 +84,18 @@ const DEFAULTS = {
|
|
|
84
84
|
// • baseDelayMs / maxDelayMs: exponential backoff bounds used when
|
|
85
85
|
// the server didn't send a Retry-After header.
|
|
86
86
|
// • jitterRatio: ± random fraction applied to backoff; 0 disables.
|
|
87
|
-
// • streamIdleTimeoutMs: per-
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
87
|
+
// • streamIdleTimeoutMs / highEffortStreamIdleTimeoutMs: per-chunk silence
|
|
88
|
+
// budgets, selected from the final wire effort (high/xhigh/max/ultra).
|
|
89
|
+
// Defaults leave 30s before the Session's 120s/300s silence watchdog.
|
|
90
|
+
// streamIdleTimeoutMs: 0 disables both; an explicit legacy timeout sets
|
|
91
|
+
// both budgets unless highEffortStreamIdleTimeoutMs is also supplied.
|
|
91
92
|
llmRetry: {
|
|
92
93
|
maxRetries: 3,
|
|
93
94
|
baseDelayMs: 1_000,
|
|
94
95
|
maxDelayMs: 30_000,
|
|
95
96
|
jitterRatio: 0.25,
|
|
96
97
|
streamIdleTimeoutMs: 90_000,
|
|
98
|
+
highEffortStreamIdleTimeoutMs: 270_000,
|
|
97
99
|
forbiddenRetryDelaysMs: [30_000, 120_000],
|
|
98
100
|
},
|
|
99
101
|
};
|
|
@@ -110,7 +112,7 @@ const DEFAULTS = {
|
|
|
110
112
|
*
|
|
111
113
|
* @param {object | null | undefined} fileConfig
|
|
112
114
|
* @param {object | null | undefined} overrides
|
|
113
|
-
* @returns {{ maxRetries: number, baseDelayMs: number, maxDelayMs: number, jitterRatio: number, streamIdleTimeoutMs: number }}
|
|
115
|
+
* @returns {{ maxRetries: number, baseDelayMs: number, maxDelayMs: number, jitterRatio: number, streamIdleTimeoutMs: number, highEffortStreamIdleTimeoutMs: number }}
|
|
114
116
|
*/
|
|
115
117
|
export function normalizeLlmRetry(fileConfig, overrides) {
|
|
116
118
|
const base = DEFAULTS.llmRetry;
|
|
@@ -131,6 +133,11 @@ export function normalizeLlmRetry(fileConfig, overrides) {
|
|
|
131
133
|
}
|
|
132
134
|
if (Number.isFinite(src.streamIdleTimeoutMs) && src.streamIdleTimeoutMs >= 0) {
|
|
133
135
|
out.streamIdleTimeoutMs = Math.min(600_000, Math.floor(src.streamIdleTimeoutMs));
|
|
136
|
+
// Preserve explicit legacy budgets (especially 0) at every effort.
|
|
137
|
+
out.highEffortStreamIdleTimeoutMs = out.streamIdleTimeoutMs;
|
|
138
|
+
}
|
|
139
|
+
if (Number.isFinite(src.highEffortStreamIdleTimeoutMs) && src.highEffortStreamIdleTimeoutMs >= 0) {
|
|
140
|
+
out.highEffortStreamIdleTimeoutMs = Math.min(600_000, Math.floor(src.highEffortStreamIdleTimeoutMs));
|
|
134
141
|
}
|
|
135
142
|
if (Array.isArray(src.forbiddenRetryDelaysMs)) {
|
|
136
143
|
out.forbiddenRetryDelaysMs = src.forbiddenRetryDelaysMs
|
package/yeaft/engine.js
CHANGED
|
@@ -1873,6 +1873,9 @@ export class Engine {
|
|
|
1873
1873
|
* string-prompt shape (no regression for existing callers).
|
|
1874
1874
|
* @param {string|null} [params.causalRootId] - Stable durable identity for
|
|
1875
1875
|
* every row generated as part of one externally accepted causal root.
|
|
1876
|
+
* @param {Function} [params.onProviderRequestStart] - Internal liveness policy
|
|
1877
|
+
* callback before dispatch: { effort, streamIdleTimeoutMs } from the final
|
|
1878
|
+
* native request. No user content; not persisted or projected to the Web.
|
|
1876
1879
|
* @yields {EngineEvent}
|
|
1877
1880
|
*/
|
|
1878
1881
|
async *query(params = {}) {
|
|
@@ -1921,7 +1924,7 @@ export class Engine {
|
|
|
1921
1924
|
}
|
|
1922
1925
|
}
|
|
1923
1926
|
|
|
1924
|
-
async *#queryLifecycle({ prompt, promptParts = null, messages = [], signal, turnConfig = null, userEffort = null, scenario = 'chat', isSubAgent = false, parentEffortDecision = null, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds = null, projectInstruction = '', projectLabel = '', vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, currentUserMessage = null, causalRootId = null, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null } = {}) {
|
|
1927
|
+
async *#queryLifecycle({ prompt, promptParts = null, messages = [], signal, turnConfig = null, userEffort = null, scenario = 'chat', isSubAgent = false, parentEffortDecision = null, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds = null, projectInstruction = '', projectLabel = '', vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, currentUserMessage = null, causalRootId = null, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null, onProviderRequestStart = null } = {}) {
|
|
1925
1928
|
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
1926
1929
|
const error = new Error('prompt is required and must be a non-empty string');
|
|
1927
1930
|
yield {
|
|
@@ -2013,7 +2016,7 @@ export class Engine {
|
|
|
2013
2016
|
try {
|
|
2014
2017
|
this.#currentThreadId = threadId || MAIN_THREAD_ID;
|
|
2015
2018
|
this.#currentCausalRootId = effectiveCausalRootId;
|
|
2016
|
-
yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, turnConfig: turnConfig ? { model: turnConfig.model, effort: turnConfig.effort, maxOutputTokens: turnConfig.maxOutputTokens } : null, userEffort: explicitUserEffort, scenario, isSubAgent, parentEffortDecision, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds, projectInstruction, projectLabel, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, currentUserMessage, causalRootId: effectiveCausalRootId, getCurrentTodos, setCurrentTodos, askUser, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, prepareProviderRequest, startProviderRequest, finishProviderRequest, failProviderRequest, closePendingUserInput, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName, retryLifecycle });
|
|
2019
|
+
yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, turnConfig: turnConfig ? { model: turnConfig.model, effort: turnConfig.effort, maxOutputTokens: turnConfig.maxOutputTokens } : null, userEffort: explicitUserEffort, scenario, isSubAgent, parentEffortDecision, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds, projectInstruction, projectLabel, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, currentUserMessage, causalRootId: effectiveCausalRootId, getCurrentTodos, setCurrentTodos, askUser, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, prepareProviderRequest, startProviderRequest, finishProviderRequest, failProviderRequest, closePendingUserInput, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName, retryLifecycle, onProviderRequestStart });
|
|
2017
2020
|
} finally {
|
|
2018
2021
|
// Closing the async generator at a visible retry boundary means the
|
|
2019
2022
|
// continuation never reached a provider. Keep it out of history and
|
|
@@ -2068,7 +2071,7 @@ export class Engine {
|
|
|
2068
2071
|
* in a try/finally without indenting the whole loop.
|
|
2069
2072
|
* @private
|
|
2070
2073
|
*/
|
|
2071
|
-
async *#runQuery({ prompt, promptParts = null, messages, signal, turnConfig = null, userEffort = null, scenario = 'chat', isSubAgent = false, parentEffortDecision = null, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds = null, projectInstruction = '', projectLabel = '', vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, currentUserMessage = null, causalRootId = null, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null, explicitSkillName = null, retryLifecycle }) {
|
|
2074
|
+
async *#runQuery({ prompt, promptParts = null, messages, signal, turnConfig = null, userEffort = null, scenario = 'chat', isSubAgent = false, parentEffortDecision = null, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds = null, projectInstruction = '', projectLabel = '', vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, currentUserMessage = null, causalRootId = null, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null, explicitSkillName = null, retryLifecycle, onProviderRequestStart = null }) {
|
|
2072
2075
|
|
|
2073
2076
|
const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
|
|
2074
2077
|
? collabToolPolicy
|
|
@@ -3124,11 +3127,12 @@ export class Engine {
|
|
|
3124
3127
|
signal,
|
|
3125
3128
|
onRawExchange: captureRawExchange,
|
|
3126
3129
|
rawExchangeMaxBytes,
|
|
3127
|
-
onRequestStart:
|
|
3130
|
+
onRequestStart: requestPolicy => {
|
|
3128
3131
|
// Native adapters invoke this immediately before fetch(). A retry
|
|
3129
3132
|
// continuation and Work Center EngineTurn become durable only when
|
|
3130
3133
|
// their request crosses dispatch, never when turn_start is shown.
|
|
3131
3134
|
commitDispatch();
|
|
3135
|
+
onProviderRequestStart?.(requestPolicy);
|
|
3132
3136
|
},
|
|
3133
3137
|
});
|
|
3134
3138
|
yield { type: 'turn_start', turnId: queryTurnId, turnNumber, threadId };
|
package/yeaft/llm/adapter.js
CHANGED
|
@@ -310,6 +310,28 @@ export class SseLineBuffer {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Resolve one request's transport silence budget after final effort enforcement.
|
|
315
|
+
* Provider/model overrides are absolute (including 0); the shared adapter is
|
|
316
|
+
* never mutated. Explicit legacy timeouts apply to every effort unless a high
|
|
317
|
+
* budget was also configured. All enabled budgets remain bounded at 10 minutes.
|
|
318
|
+
*
|
|
319
|
+
* @param {{ streamIdleTimeoutMs?: number, highEffortStreamIdleTimeoutMs?: number }} policy
|
|
320
|
+
* @param {string|null} effort — effective wire effort, not the requested value
|
|
321
|
+
* @param {number|undefined} overrideMs — model > provider override
|
|
322
|
+
* @returns {number}
|
|
323
|
+
*/
|
|
324
|
+
export function resolveStreamIdleTimeoutMs(policy = {}, effort = null, overrideMs) {
|
|
325
|
+
const valid = value => Number.isFinite(value) && value >= 0;
|
|
326
|
+
const clamp = value => Math.min(600_000, Math.floor(value));
|
|
327
|
+
if (valid(overrideMs)) return clamp(overrideMs);
|
|
328
|
+
const normal = valid(policy.streamIdleTimeoutMs) ? clamp(policy.streamIdleTimeoutMs) : 0;
|
|
329
|
+
if (normal === 0) return 0;
|
|
330
|
+
const high = ['high', 'xhigh', 'max', 'ultra'].includes(effort);
|
|
331
|
+
return high && valid(policy.highEffortStreamIdleTimeoutMs)
|
|
332
|
+
? clamp(policy.highEffortStreamIdleTimeoutMs) : normal;
|
|
333
|
+
}
|
|
334
|
+
|
|
313
335
|
/**
|
|
314
336
|
* Read one chunk from a Fetch stream with a silence timeout. This is not a
|
|
315
337
|
* total request deadline: every received chunk gets a fresh budget. A caller
|
|
@@ -692,6 +714,7 @@ export async function createLLMAdapter(config) {
|
|
|
692
714
|
apiKey: config.apiKey,
|
|
693
715
|
baseUrl: config.baseUrl || undefined, // AnthropicAdapter has its own default
|
|
694
716
|
streamIdleTimeoutMs: config.llmRetry?.streamIdleTimeoutMs,
|
|
717
|
+
highEffortStreamIdleTimeoutMs: config.llmRetry?.highEffortStreamIdleTimeoutMs,
|
|
695
718
|
});
|
|
696
719
|
}
|
|
697
720
|
|
package/yeaft/llm/anthropic.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
LLMServerError,
|
|
19
19
|
LLMAbortError,
|
|
20
20
|
readStreamChunkWithIdleTimeout,
|
|
21
|
+
resolveStreamIdleTimeoutMs,
|
|
21
22
|
redactRawRequest,
|
|
22
23
|
safeHeaders,
|
|
23
24
|
SseLineBuffer,
|
|
@@ -71,6 +72,13 @@ function hasNonEmptyText(value) {
|
|
|
71
72
|
return typeof value === 'string' && value.trim().length > 0;
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
function requireToolInputObject(input) {
|
|
76
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
77
|
+
throw new LLMServerError('Anthropic returned invalid or incomplete tool input', 0);
|
|
78
|
+
}
|
|
79
|
+
return input;
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
function translateUserContent(content) {
|
|
75
83
|
if (hasNonEmptyText(content)) return content;
|
|
76
84
|
|
|
@@ -102,17 +110,17 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
102
110
|
#apiKey;
|
|
103
111
|
#baseUrl;
|
|
104
112
|
#authHeaderMode;
|
|
105
|
-
#
|
|
113
|
+
#streamIdlePolicy;
|
|
106
114
|
|
|
107
115
|
/**
|
|
108
|
-
* @param {{ apiKey: string, baseUrl?: string, authHeaderMode?: 'x-api-key'|'bearer', streamIdleTimeoutMs?: number }} config
|
|
116
|
+
* @param {{ apiKey: string, baseUrl?: string, authHeaderMode?: 'x-api-key'|'bearer', streamIdleTimeoutMs?: number, highEffortStreamIdleTimeoutMs?: number }} config
|
|
109
117
|
*/
|
|
110
|
-
constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, authHeaderMode = 'x-api-key', streamIdleTimeoutMs = 0 }) {
|
|
111
|
-
super({ apiKey, baseUrl, streamIdleTimeoutMs });
|
|
118
|
+
constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, authHeaderMode = 'x-api-key', streamIdleTimeoutMs = 0, highEffortStreamIdleTimeoutMs }) {
|
|
119
|
+
super({ apiKey, baseUrl, streamIdleTimeoutMs, highEffortStreamIdleTimeoutMs });
|
|
112
120
|
this.#apiKey = apiKey;
|
|
113
121
|
this.#baseUrl = baseUrl;
|
|
114
122
|
this.#authHeaderMode = authHeaderMode === 'bearer' ? 'bearer' : 'x-api-key';
|
|
115
|
-
this.#
|
|
123
|
+
this.#streamIdlePolicy = { streamIdleTimeoutMs, highEffortStreamIdleTimeoutMs };
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
#headers() {
|
|
@@ -133,12 +141,15 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
133
141
|
* @param {import('./adapter.js').UnifiedToolDef[]} tools
|
|
134
142
|
* @returns {object[]}
|
|
135
143
|
*/
|
|
136
|
-
#translateTools(tools) {
|
|
144
|
+
#translateTools(tools, context) {
|
|
137
145
|
if (!tools || tools.length === 0) return undefined;
|
|
138
146
|
return tools.map(t => ({
|
|
139
147
|
name: t.name,
|
|
140
148
|
description: t.description,
|
|
141
149
|
input_schema: t.parameters,
|
|
150
|
+
// Native API supports per-tool streaming without the legacy beta header.
|
|
151
|
+
// Unknown/translation gateways must explicitly opt in after verification.
|
|
152
|
+
...(context?.capabilities?.eagerInputStreaming ? { eager_input_streaming: true } : {}),
|
|
142
153
|
}));
|
|
143
154
|
}
|
|
144
155
|
|
|
@@ -238,7 +249,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
238
249
|
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'xhigh'|'max', effortSource?: 'user'|'auto', effortContext?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
|
|
239
250
|
* @returns {AsyncGenerator<import('./adapter.js').StreamEvent>}
|
|
240
251
|
*/
|
|
241
|
-
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext, extraBody, providerContext, requestIdentity, onProviderDiagnostics, effortConstraint = null, onEffortDecision = null, signal, onRawExchange, rawExchangeMaxBytes = 512 * 1024, onRequestStart }) {
|
|
252
|
+
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext, extraBody, providerContext, requestIdentity, onProviderDiagnostics, effortConstraint = null, onEffortDecision = null, signal, onRawExchange, rawExchangeMaxBytes = 512 * 1024, onRequestStart, streamIdleTimeoutMs }) {
|
|
242
253
|
if (signal?.aborted) throw new LLMAbortError();
|
|
243
254
|
|
|
244
255
|
const context = providerContext || createProviderContext({ protocol: 'anthropic', baseUrl: this.#baseUrl, model });
|
|
@@ -259,7 +270,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
259
270
|
applyAnthropicThinking(body, model, normEffort, effortContext);
|
|
260
271
|
}
|
|
261
272
|
|
|
262
|
-
const translatedTools = this.#translateTools(tools);
|
|
273
|
+
const translatedTools = this.#translateTools(tools, context);
|
|
263
274
|
if (translatedTools) body.tools = translatedTools;
|
|
264
275
|
if (extraBody) Object.assign(body, extraBody);
|
|
265
276
|
body.model = model; // Do not let extraBody bypass origin/model ownership.
|
|
@@ -271,6 +282,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
271
282
|
: captureEffortDecision({ body, model, protocol: 'anthropic', effortContext, requested: effort, source: effortSource || 'scenario' });
|
|
272
283
|
onEffortDecision?.(effortDecision);
|
|
273
284
|
const wireBody = toWellFormedJson(body);
|
|
285
|
+
const idleMs = resolveStreamIdleTimeoutMs(this.#streamIdlePolicy, effortDecision.effective, streamIdleTimeoutMs);
|
|
274
286
|
|
|
275
287
|
const url = `${this.#baseUrl}/v1/messages`;
|
|
276
288
|
const headers = this.#headers();
|
|
@@ -282,7 +294,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
282
294
|
|
|
283
295
|
let response;
|
|
284
296
|
try {
|
|
285
|
-
onRequestStart?.();
|
|
297
|
+
onRequestStart?.({ effort: effortDecision.effective, streamIdleTimeoutMs: idleMs });
|
|
286
298
|
response = await fetch(url, {
|
|
287
299
|
method: 'POST',
|
|
288
300
|
headers,
|
|
@@ -315,6 +327,19 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
315
327
|
// when streaming was requested. Seal exactly the same native blocks.
|
|
316
328
|
if ((response.headers?.get('content-type') || '').includes('application/json')) {
|
|
317
329
|
const result = await response.json();
|
|
330
|
+
// Account for consumed tokens even when the completed response cannot
|
|
331
|
+
// safely publish tools. Budget consumers may abort on this usage event.
|
|
332
|
+
yield { type: 'usage', inputTokens: result.usage?.input_tokens || 0, outputTokens: result.usage?.output_tokens || 0,
|
|
333
|
+
cacheReadTokens: result.usage?.cache_read_input_tokens || 0, cacheWriteTokens: result.usage?.cache_creation_input_tokens || 0,
|
|
334
|
+
...reasoningUsage(result.usage, 'anthropic') };
|
|
335
|
+
if (signal?.aborted) throw new LLMAbortError();
|
|
336
|
+
for (const block of result.content || []) {
|
|
337
|
+
if (block.type !== 'tool_use') continue;
|
|
338
|
+
requireToolInputObject(block.input);
|
|
339
|
+
if (result.stop_reason === 'max_tokens') {
|
|
340
|
+
throw new LLMServerError('Anthropic tool response was truncated at max_tokens', 0);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
318
343
|
const state = createProviderState({ context, identity: requestIdentity, items: result.content, responseId: result.id });
|
|
319
344
|
for (const block of result.content || []) {
|
|
320
345
|
if (block.type === 'text') yield { type: 'text_delta', text: block.text };
|
|
@@ -323,9 +348,6 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
323
348
|
if (block.type === 'redacted_thinking') yield { type: 'thinking_block_end', redacted: true, data: block.data };
|
|
324
349
|
}
|
|
325
350
|
if (state) yield { type: 'provider_state', providerState: state, providerStateBytes: providerStateBytes(state) };
|
|
326
|
-
yield { type: 'usage', inputTokens: result.usage?.input_tokens || 0, outputTokens: result.usage?.output_tokens || 0,
|
|
327
|
-
cacheReadTokens: result.usage?.cache_read_input_tokens || 0, cacheWriteTokens: result.usage?.cache_creation_input_tokens || 0,
|
|
328
|
-
...reasoningUsage(result.usage, 'anthropic') };
|
|
329
351
|
yield { type: 'stop', stopReason: this.#mapStopReason(result.stop_reason) };
|
|
330
352
|
if (onRawExchange) {
|
|
331
353
|
try { onRawExchange({ rawRequest, rawResponse: { status: response.status, headers: safeHeaders(response), body: result } }); } catch { /* diagnostic only */ }
|
|
@@ -365,12 +387,20 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
365
387
|
let sawStop = false;
|
|
366
388
|
let sawMessageStart = false;
|
|
367
389
|
let cumulativeOutputTokens = 0;
|
|
390
|
+
let toolInputError = null;
|
|
391
|
+
const hasToolBlocks = () => toolInputError !== null || [...blockByIndex.values()].some(block => block.kind === 'tool_use')
|
|
392
|
+
|| [...completedBlocks.values()].some(block => block.type === 'tool_use');
|
|
393
|
+
const requireClosedBlocks = () => {
|
|
394
|
+
if (blockByIndex.size > 0) {
|
|
395
|
+
throw new LLMServerError('Anthropic stream ended with an incomplete content block', 0);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
368
398
|
|
|
369
399
|
try {
|
|
370
|
-
while (
|
|
400
|
+
while (!sawStop) {
|
|
371
401
|
const { done, value } = await readStreamChunkWithIdleTimeout(reader, {
|
|
372
402
|
signal,
|
|
373
|
-
idleMs
|
|
403
|
+
idleMs,
|
|
374
404
|
providerLabel: 'Anthropic',
|
|
375
405
|
});
|
|
376
406
|
if (done) break;
|
|
@@ -380,9 +410,17 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
380
410
|
const lines = sseLines.push(chunkText);
|
|
381
411
|
|
|
382
412
|
for (const line of lines) {
|
|
383
|
-
|
|
384
|
-
|
|
413
|
+
// SSE permits `data:{...}` as well as `data: {...}`. Skipping the
|
|
414
|
+
// compact form can lose input deltas or the max_tokens stop reason.
|
|
415
|
+
if (!line.startsWith('data:')) continue;
|
|
416
|
+
const data = line.slice(5).trim();
|
|
417
|
+
if (signal?.aborted) throw new LLMAbortError();
|
|
418
|
+
if (sawStop) continue;
|
|
385
419
|
if (data === '[DONE]') {
|
|
420
|
+
// Legacy text-only gateways may use [DONE]. Tools require the
|
|
421
|
+
// native message_stop boundary; never seal a partial batch here.
|
|
422
|
+
if (hasToolBlocks()) throw new LLMServerError('Anthropic stream ended before stop event', 0);
|
|
423
|
+
requireClosedBlocks();
|
|
386
424
|
sawStop = true;
|
|
387
425
|
continue;
|
|
388
426
|
}
|
|
@@ -391,12 +429,12 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
391
429
|
try {
|
|
392
430
|
event = JSON.parse(data);
|
|
393
431
|
} catch {
|
|
394
|
-
|
|
432
|
+
// A lost input delta can otherwise turn the start placeholder {}
|
|
433
|
+
// into executable arguments. Do not silently skip malformed SSE.
|
|
434
|
+
throw new LLMServerError('Anthropic returned a malformed stream event', 0);
|
|
395
435
|
}
|
|
396
436
|
|
|
397
|
-
const type = event
|
|
398
|
-
if (signal?.aborted) throw new LLMAbortError();
|
|
399
|
-
if (sawStop) continue;
|
|
437
|
+
const type = event?.type;
|
|
400
438
|
|
|
401
439
|
if (type === 'content_block_start') {
|
|
402
440
|
const block = event.content_block;
|
|
@@ -466,20 +504,16 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
466
504
|
// Unknown / unhandled block kind (e.g. text — we don't track
|
|
467
505
|
// text state because text_delta is forwarded immediately).
|
|
468
506
|
} else if (st.kind === 'tool_use') {
|
|
469
|
-
let parsedInput = {};
|
|
470
507
|
try {
|
|
471
|
-
parsedInput = st.input ? JSON.parse(st.input) : st.native.input
|
|
508
|
+
const parsedInput = requireToolInputObject(st.input ? JSON.parse(st.input) : st.native.input);
|
|
509
|
+
completedBlocks.set(idx, { ...st.native, input: parsedInput });
|
|
472
510
|
} catch {
|
|
473
|
-
|
|
474
|
-
|
|
511
|
+
// Reject the entire batch, but drain to the message boundary so
|
|
512
|
+
// the provider's final usage is not lost on truncated JSON.
|
|
513
|
+
toolInputError = new LLMServerError('Anthropic returned invalid or incomplete tool input', 0);
|
|
475
514
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
type: 'tool_call',
|
|
479
|
-
id: st.id,
|
|
480
|
-
name: st.name,
|
|
481
|
-
input: parsedInput,
|
|
482
|
-
};
|
|
515
|
+
// Publish tools only after message_stop validates the entire
|
|
516
|
+
// response, including parallel siblings and max_tokens truncation.
|
|
483
517
|
} else if (st.kind === 'thinking' || st.kind === 'redacted_thinking') {
|
|
484
518
|
// task-327d: emit ONE end-of-block event with the assembled
|
|
485
519
|
// payload + signature. Engine collects these for replay.
|
|
@@ -505,15 +539,8 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
505
539
|
if (st?.kind === 'redacted_thinking') completedBlocks.set(idx, { type: 'redacted_thinking', data: st.data });
|
|
506
540
|
blockByIndex.delete(idx);
|
|
507
541
|
} else if (type === 'message_delta') {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
// Only message_stop seals native state. EOF after message_delta
|
|
511
|
-
// must not silently complete a signed tool turn without its state.
|
|
512
|
-
yield {
|
|
513
|
-
type: 'stop',
|
|
514
|
-
stopReason: this.#mapStopReason(stopReason),
|
|
515
|
-
};
|
|
516
|
-
}
|
|
542
|
+
// Account before validating stop_reason: failed requests still cost
|
|
543
|
+
// tokens, and usage can trigger a budget abort before any retry.
|
|
517
544
|
// Anthropic message_delta usage is cumulative across the response.
|
|
518
545
|
// Expose only the newly consumed output tokens so shared accounting
|
|
519
546
|
// can safely add events from message_start and multiple deltas.
|
|
@@ -534,11 +561,27 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
534
561
|
outputTokens,
|
|
535
562
|
};
|
|
536
563
|
}
|
|
564
|
+
if (signal?.aborted) throw new LLMAbortError();
|
|
565
|
+
const stopReason = event.delta?.stop_reason;
|
|
566
|
+
if (stopReason === 'max_tokens' && hasToolBlocks()) {
|
|
567
|
+
toolInputError = new LLMServerError('Anthropic tool response was truncated at max_tokens', 0);
|
|
568
|
+
}
|
|
569
|
+
if (stopReason && !toolInputError) {
|
|
570
|
+
// Only message_stop seals native state. EOF after message_delta
|
|
571
|
+
// must not silently complete a signed tool turn without its state.
|
|
572
|
+
yield { type: 'stop', stopReason: this.#mapStopReason(stopReason) };
|
|
573
|
+
}
|
|
537
574
|
} else if (type === 'message_stop') {
|
|
575
|
+
if (toolInputError) throw toolInputError;
|
|
576
|
+
requireClosedBlocks();
|
|
538
577
|
sawStop = true;
|
|
539
|
-
if (!stateFailed
|
|
540
|
-
const
|
|
541
|
-
|
|
578
|
+
if (!stateFailed) {
|
|
579
|
+
const items = [...completedBlocks].sort(([a], [b]) => a - b).map(([, block]) => block);
|
|
580
|
+
const state = createProviderState({ context, identity: requestIdentity, responseId, items });
|
|
581
|
+
for (const block of items) {
|
|
582
|
+
if (signal?.aborted) throw new LLMAbortError();
|
|
583
|
+
if (block.type === 'tool_use') yield { type: 'tool_call', id: block.id, name: block.name, input: block.input };
|
|
584
|
+
}
|
|
542
585
|
if (state) yield { type: 'provider_state', providerState: state, providerStateBytes: providerStateBytes(state) };
|
|
543
586
|
}
|
|
544
587
|
} else if (type === 'message_start') {
|
|
@@ -571,12 +614,18 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
571
614
|
}
|
|
572
615
|
}
|
|
573
616
|
}
|
|
574
|
-
if (
|
|
617
|
+
if (signal?.aborted) throw new LLMAbortError();
|
|
618
|
+
if (toolInputError) throw toolInputError;
|
|
619
|
+
if (!sawStop && (sawMessageStart || hasToolBlocks())) {
|
|
575
620
|
throw new LLMServerError('Anthropic stream ended before stop event', 0);
|
|
576
621
|
}
|
|
622
|
+
requireClosedBlocks();
|
|
577
623
|
} catch (err) {
|
|
578
624
|
throw classifyFetchError(err, { providerLabel: 'Anthropic', signal });
|
|
579
625
|
} finally {
|
|
626
|
+
// Close malformed/aborted streams and transports that stay open after
|
|
627
|
+
// message_stop. Cancellation must not delay the caller or mask its error.
|
|
628
|
+
try { Promise.resolve(reader.cancel()).catch(() => {}); } catch { /* best-effort */ }
|
|
580
629
|
reader.releaseLock();
|
|
581
630
|
// Emit raw exchange after stream completes (or errors). Body is the
|
|
582
631
|
// verbatim SSE — never truncated.
|
|
@@ -72,8 +72,24 @@ export function modelEntryForGitHubCopilot(id) {
|
|
|
72
72
|
return protocol ? { id: value, protocol } : { id: value };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Managed catalogs still own model protocols and credentials. Preserve only
|
|
76
|
+
// supported, non-secret request-policy overrides across normalization and save.
|
|
77
|
+
function requestPolicyOverrides(entry) {
|
|
78
|
+
const out = {};
|
|
79
|
+
if (entry?.capabilities && typeof entry.capabilities === 'object' && !Array.isArray(entry.capabilities)) {
|
|
80
|
+
out.capabilities = { ...entry.capabilities };
|
|
81
|
+
}
|
|
82
|
+
if (Number.isFinite(entry?.streamIdleTimeoutMs) && entry.streamIdleTimeoutMs >= 0) {
|
|
83
|
+
out.streamIdleTimeoutMs = Math.min(600_000, Math.floor(entry.streamIdleTimeoutMs));
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
export function githubCopilotModelEntries(ids = FALLBACK_GITHUB_COPILOT_MODELS) {
|
|
76
|
-
return dedupe(ids).map(item =>
|
|
89
|
+
return dedupe(ids).map(item => ({
|
|
90
|
+
...modelEntryForGitHubCopilot(modelId(item)),
|
|
91
|
+
...requestPolicyOverrides(item),
|
|
92
|
+
}));
|
|
77
93
|
}
|
|
78
94
|
|
|
79
95
|
export function normalizeKnownProviderForRuntime(provider) {
|
|
@@ -100,6 +116,7 @@ export function serializeKnownProviderForPersistence(provider) {
|
|
|
100
116
|
name: provider.name || GITHUB_COPILOT_PROVIDER_NAME,
|
|
101
117
|
credentialProvider: GITHUB_COPILOT_CREDENTIAL_PROVIDER,
|
|
102
118
|
managed: provider.managed || GITHUB_COPILOT_CREDENTIAL_PROVIDER,
|
|
119
|
+
...requestPolicyOverrides(provider),
|
|
103
120
|
...(models.length ? { models } : {}),
|
|
104
121
|
};
|
|
105
122
|
}
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
classifyFetchError,
|
|
39
39
|
retryAfterFromResponse,
|
|
40
40
|
readStreamChunkWithIdleTimeout,
|
|
41
|
+
resolveStreamIdleTimeoutMs,
|
|
41
42
|
redactRawRequest,
|
|
42
43
|
safeHeaders,
|
|
43
44
|
SseLineBuffer,
|
|
@@ -76,16 +77,16 @@ function effortForResponses(effort) {
|
|
|
76
77
|
export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
77
78
|
#apiKey;
|
|
78
79
|
#baseUrl;
|
|
79
|
-
#
|
|
80
|
+
#streamIdlePolicy;
|
|
80
81
|
|
|
81
82
|
/**
|
|
82
|
-
* @param {{ apiKey: string, baseUrl?: string, streamIdleTimeoutMs?: number }} config
|
|
83
|
+
* @param {{ apiKey: string, baseUrl?: string, streamIdleTimeoutMs?: number, highEffortStreamIdleTimeoutMs?: number }} config
|
|
83
84
|
*/
|
|
84
|
-
constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, streamIdleTimeoutMs = 0 }) {
|
|
85
|
-
super({ apiKey, baseUrl, streamIdleTimeoutMs });
|
|
85
|
+
constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, streamIdleTimeoutMs = 0, highEffortStreamIdleTimeoutMs }) {
|
|
86
|
+
super({ apiKey, baseUrl, streamIdleTimeoutMs, highEffortStreamIdleTimeoutMs });
|
|
86
87
|
this.#apiKey = apiKey;
|
|
87
88
|
this.#baseUrl = (baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
88
|
-
this.#
|
|
89
|
+
this.#streamIdlePolicy = { streamIdleTimeoutMs, highEffortStreamIdleTimeoutMs };
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
/** Expose baseUrl for testing. */
|
|
@@ -268,7 +269,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
268
269
|
* `api-key` headers are auto-redacted (see `redactRawRequest` in
|
|
269
270
|
* `adapter.js`); request-body fields are caller-controlled.
|
|
270
271
|
*/
|
|
271
|
-
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext = {}, extraBody, providerContext, requestIdentity, onProviderDiagnostics, effortConstraint = null, onEffortDecision = null, signal, onRawExchange, rawExchangeMaxBytes = 512 * 1024, onRequestStart }) {
|
|
272
|
+
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext = {}, extraBody, providerContext, requestIdentity, onProviderDiagnostics, effortConstraint = null, onEffortDecision = null, signal, onRawExchange, rawExchangeMaxBytes = 512 * 1024, onRequestStart, streamIdleTimeoutMs }) {
|
|
272
273
|
if (signal?.aborted) throw new LLMAbortError();
|
|
273
274
|
|
|
274
275
|
const context = providerContext || createProviderContext({ protocol: 'openai-responses', baseUrl: this.#baseUrl, model });
|
|
@@ -305,6 +306,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
305
306
|
: captureEffortDecision({ body, model, protocol: 'openai-responses', effortContext, requested: effort, source: effortSource || 'scenario' });
|
|
306
307
|
onEffortDecision?.(effortDecision);
|
|
307
308
|
const wireBody = toWellFormedJson(body);
|
|
309
|
+
const idleMs = resolveStreamIdleTimeoutMs(this.#streamIdlePolicy, effortDecision.effective, streamIdleTimeoutMs);
|
|
308
310
|
|
|
309
311
|
const url = `${this.#baseUrl}/responses`;
|
|
310
312
|
const headers = {
|
|
@@ -318,7 +320,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
318
320
|
|
|
319
321
|
let response;
|
|
320
322
|
try {
|
|
321
|
-
onRequestStart?.();
|
|
323
|
+
onRequestStart?.({ effort: effortDecision.effective, streamIdleTimeoutMs: idleMs });
|
|
322
324
|
response = await fetch(url, {
|
|
323
325
|
method: 'POST',
|
|
324
326
|
headers,
|
|
@@ -402,7 +404,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
402
404
|
while (true) {
|
|
403
405
|
const { done, value } = await readStreamChunkWithIdleTimeout(reader, {
|
|
404
406
|
signal,
|
|
405
|
-
idleMs
|
|
407
|
+
idleMs,
|
|
406
408
|
providerLabel: 'OpenAI',
|
|
407
409
|
});
|
|
408
410
|
if (done) break;
|
|
@@ -75,6 +75,7 @@ export function createProviderContext({ protocol, baseUrl, providerId, credentia
|
|
|
75
75
|
nativeReasoningState: enabled('nativeReasoningState'),
|
|
76
76
|
promptCaching: enabled('promptCaching'),
|
|
77
77
|
parallelToolCalls: enabled('parallelToolCalls'),
|
|
78
|
+
eagerInputStreaming: protocol === 'anthropic' && enabled('eagerInputStreaming'),
|
|
78
79
|
},
|
|
79
80
|
diagnostics: { capabilitySource: capabilities.translation ? 'translation-disabled' : native ? 'native-endpoint' : 'custom-endpoint-unverified' },
|
|
80
81
|
};
|
package/yeaft/llm/router.js
CHANGED
|
@@ -48,6 +48,9 @@ export function normalizeModelEntry(entry) {
|
|
|
48
48
|
if (entry && typeof entry === 'object' && typeof entry.id === 'string' && entry.id) {
|
|
49
49
|
const out = { id: entry.id };
|
|
50
50
|
if (entry.capabilities && typeof entry.capabilities === 'object') out.capabilities = { ...entry.capabilities };
|
|
51
|
+
if (Number.isFinite(entry.streamIdleTimeoutMs) && entry.streamIdleTimeoutMs >= 0) {
|
|
52
|
+
out.streamIdleTimeoutMs = Math.min(600_000, Math.floor(entry.streamIdleTimeoutMs));
|
|
53
|
+
}
|
|
51
54
|
if (typeof entry.protocol === 'string' && entry.protocol) {
|
|
52
55
|
out.protocol = entry.protocol;
|
|
53
56
|
}
|
|
@@ -246,18 +249,19 @@ export class AdapterRouter extends LLMAdapter {
|
|
|
246
249
|
/** @type {Set<string>} managed providers backed by an explicit model catalog */
|
|
247
250
|
#authoritativeManagedProviders;
|
|
248
251
|
|
|
249
|
-
/** @type {
|
|
250
|
-
#
|
|
252
|
+
/** @type {{ streamIdleTimeoutMs?: number, highEffortStreamIdleTimeoutMs?: number }} */
|
|
253
|
+
#streamIdlePolicy;
|
|
251
254
|
|
|
252
255
|
/**
|
|
253
|
-
* @param {{ providers: object[], llmRetry?: { streamIdleTimeoutMs?: number } }} params
|
|
256
|
+
* @param {{ providers: object[], llmRetry?: { streamIdleTimeoutMs?: number, highEffortStreamIdleTimeoutMs?: number } }} params
|
|
254
257
|
* @param {object[]} params.providers — Array of { name, baseUrl, apiKey, protocol?, models[] }
|
|
255
258
|
*/
|
|
256
259
|
constructor({ providers, llmRetry = {} }) {
|
|
257
260
|
super();
|
|
258
|
-
this.#
|
|
259
|
-
|
|
260
|
-
:
|
|
261
|
+
this.#streamIdlePolicy = {
|
|
262
|
+
streamIdleTimeoutMs: llmRetry.streamIdleTimeoutMs,
|
|
263
|
+
highEffortStreamIdleTimeoutMs: llmRetry.highEffortStreamIdleTimeoutMs,
|
|
264
|
+
};
|
|
261
265
|
this.#providers = [];
|
|
262
266
|
this.#modelToProvider = new Map();
|
|
263
267
|
this.#adapterCache = new Map();
|
|
@@ -509,7 +513,7 @@ export class AdapterRouter extends LLMAdapter {
|
|
|
509
513
|
apiKey,
|
|
510
514
|
baseUrl: provider.baseUrl,
|
|
511
515
|
authHeaderMode: anthropicAuthHeaderMode,
|
|
512
|
-
|
|
516
|
+
...this.#streamIdlePolicy,
|
|
513
517
|
});
|
|
514
518
|
} else if (protocol === 'openai-responses') {
|
|
515
519
|
// OpenAI Responses API (/v1/responses) — canonical OpenAI-compatible path.
|
|
@@ -517,7 +521,7 @@ export class AdapterRouter extends LLMAdapter {
|
|
|
517
521
|
adapter = new OpenAIResponsesAdapter({
|
|
518
522
|
apiKey,
|
|
519
523
|
baseUrl: provider.baseUrl,
|
|
520
|
-
|
|
524
|
+
...this.#streamIdlePolicy,
|
|
521
525
|
});
|
|
522
526
|
} else {
|
|
523
527
|
throw new Error(
|
|
@@ -659,8 +663,13 @@ export class AdapterRouter extends LLMAdapter {
|
|
|
659
663
|
};
|
|
660
664
|
const filtered = filterEffortForModel({ ...params, model: resolved.modelId }, resolved);
|
|
661
665
|
const sanitized = sanitizeMessagesForWire(filtered);
|
|
666
|
+
// Resolve the absolute model > provider override per request, never on
|
|
667
|
+
// the shared adapter: two models may use the same cached instance.
|
|
668
|
+
const configuredTimeout = [resolved.entry?.streamIdleTimeoutMs, provider?.streamIdleTimeoutMs]
|
|
669
|
+
.find(value => Number.isFinite(value) && value >= 0);
|
|
662
670
|
try {
|
|
663
|
-
yield* resolved.adapter.stream({ ...sanitized, model: resolved.modelId, effortContext, providerContext,
|
|
671
|
+
yield* resolved.adapter.stream({ ...sanitized, model: resolved.modelId, effortContext, providerContext,
|
|
672
|
+
streamIdleTimeoutMs: configuredTimeout, rawExchangeMaxBytes: params.rawExchangeMaxBytes });
|
|
664
673
|
return;
|
|
665
674
|
} catch (err) {
|
|
666
675
|
this.#annotateAuthError(err, provider, params.model);
|
|
@@ -71,9 +71,9 @@ export class UsageAccountingAdapter extends LLMAdapter {
|
|
|
71
71
|
const upstream = params?.onRequestStart;
|
|
72
72
|
return {
|
|
73
73
|
...params,
|
|
74
|
-
onRequestStart: () => {
|
|
74
|
+
onRequestStart: (...args) => {
|
|
75
75
|
try {
|
|
76
|
-
upstream?.();
|
|
76
|
+
upstream?.(...args);
|
|
77
77
|
} finally {
|
|
78
78
|
try {
|
|
79
79
|
this.#onRequest();
|