@yeaft/webchat-agent 1.0.302 → 1.0.304
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/package.json +1 -1
- package/yeaft/config.js +7 -0
- package/yeaft/engine.js +38 -1
- package/yeaft/llm/adapter.js +57 -2
- package/yeaft/llm/anthropic.js +2 -1
- package/yeaft/llm/credentials/github-copilot.js +29 -5
- package/yeaft/llm/credentials/index.js +13 -1
- package/yeaft/llm/openai-responses.js +2 -1
- package/yeaft/llm/router.js +78 -23
- package/yeaft/web-bridge.js +10 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.304"}
|
package/package.json
CHANGED
package/yeaft/config.js
CHANGED
|
@@ -77,6 +77,7 @@ const DEFAULTS = {
|
|
|
77
77
|
maxDelayMs: 30_000,
|
|
78
78
|
jitterRatio: 0.25,
|
|
79
79
|
streamIdleTimeoutMs: 90_000,
|
|
80
|
+
forbiddenRetryDelaysMs: [30_000, 120_000],
|
|
80
81
|
},
|
|
81
82
|
};
|
|
82
83
|
|
|
@@ -114,6 +115,12 @@ export function normalizeLlmRetry(fileConfig, overrides) {
|
|
|
114
115
|
if (Number.isFinite(src.streamIdleTimeoutMs) && src.streamIdleTimeoutMs >= 0) {
|
|
115
116
|
out.streamIdleTimeoutMs = Math.min(600_000, Math.floor(src.streamIdleTimeoutMs));
|
|
116
117
|
}
|
|
118
|
+
if (Array.isArray(src.forbiddenRetryDelaysMs)) {
|
|
119
|
+
out.forbiddenRetryDelaysMs = src.forbiddenRetryDelaysMs
|
|
120
|
+
.filter(v => Number.isFinite(v) && v >= 0)
|
|
121
|
+
.slice(0, 3)
|
|
122
|
+
.map(v => Math.min(600_000, Math.floor(v)));
|
|
123
|
+
}
|
|
117
124
|
};
|
|
118
125
|
apply(fileConfig);
|
|
119
126
|
apply(overrides);
|
package/yeaft/engine.js
CHANGED
|
@@ -22,7 +22,7 @@ import { promises as fsp } from 'fs';
|
|
|
22
22
|
import { join, resolve as resolvePath } from 'path';
|
|
23
23
|
import { buildSystemPrompt, buildWorkerPrompt } from './prompts.js';
|
|
24
24
|
import { getRuntimePlatformInfo } from './runtime-platform.js';
|
|
25
|
-
import { LLMContextError, LLMAbortError, LLMRateLimitError, LLMServerError, LLMStreamIdleTimeoutError } from './llm/adapter.js';
|
|
25
|
+
import { LLMContextError, LLMAbortError, LLMAuthError, LLMRateLimitError, LLMServerError, LLMStreamIdleTimeoutError } from './llm/adapter.js';
|
|
26
26
|
import { runMemoryPreflow, buildRelevantScopes, memoryScopeLabel } from './sessions/pre-flow.js';
|
|
27
27
|
import { readProjectDoc, pickProjectDocFile, DEFAULT_PROJECT_DOC_MAX_BYTES } from './sessions/project-doc.js';
|
|
28
28
|
import { partitionMessages } from './compact/partition.js';
|
|
@@ -103,6 +103,7 @@ const RETRY_DEFAULTS = Object.freeze({
|
|
|
103
103
|
baseDelayMs: 1_000,
|
|
104
104
|
maxDelayMs: 30_000,
|
|
105
105
|
jitterRatio: 0.25,
|
|
106
|
+
forbiddenRetryDelaysMs: [30_000, 120_000],
|
|
106
107
|
});
|
|
107
108
|
|
|
108
109
|
const RETRY_CONTINUATION_PROMPT =
|
|
@@ -159,11 +160,18 @@ function stripLeadingSkillCommandFromPromptParts(promptParts, skillManager) {
|
|
|
159
160
|
function resolveRetryPolicy(config) {
|
|
160
161
|
const raw = config?.llmRetry || {};
|
|
161
162
|
const num = (v, d) => (Number.isFinite(v) && v >= 0 ? v : d);
|
|
163
|
+
const forbiddenRetryDelaysMs = Array.isArray(raw.forbiddenRetryDelaysMs)
|
|
164
|
+
? raw.forbiddenRetryDelaysMs
|
|
165
|
+
.filter(v => Number.isFinite(v) && v >= 0)
|
|
166
|
+
.slice(0, 3)
|
|
167
|
+
.map(v => Math.min(600_000, Math.floor(v)))
|
|
168
|
+
: RETRY_DEFAULTS.forbiddenRetryDelaysMs;
|
|
162
169
|
return {
|
|
163
170
|
maxRetries: Math.max(0, Math.floor(num(raw.maxRetries, RETRY_DEFAULTS.maxRetries))),
|
|
164
171
|
baseDelayMs: Math.max(0, Math.floor(num(raw.baseDelayMs, RETRY_DEFAULTS.baseDelayMs))),
|
|
165
172
|
maxDelayMs: Math.max(0, Math.floor(num(raw.maxDelayMs, RETRY_DEFAULTS.maxDelayMs))),
|
|
166
173
|
jitterRatio: Math.min(1, Math.max(0, num(raw.jitterRatio, RETRY_DEFAULTS.jitterRatio))),
|
|
174
|
+
forbiddenRetryDelaysMs,
|
|
167
175
|
};
|
|
168
176
|
}
|
|
169
177
|
|
|
@@ -2408,6 +2416,7 @@ export class Engine {
|
|
|
2408
2416
|
// and does NOT count against this budget.
|
|
2409
2417
|
const retryPolicy = resolveRetryPolicy(this.#config);
|
|
2410
2418
|
let consecutiveRetryableErrors = 0;
|
|
2419
|
+
let consecutiveForbiddenErrors = 0;
|
|
2411
2420
|
|
|
2412
2421
|
while (true) {
|
|
2413
2422
|
turnNumber++;
|
|
@@ -2949,6 +2958,34 @@ export class Engine {
|
|
|
2949
2958
|
// leave ambiguous execution ownership, so only pre-tool failures are
|
|
2950
2959
|
// eligible for transparent retry or model fallback.
|
|
2951
2960
|
const canReplayProviderRequest = toolCalls.length === 0;
|
|
2961
|
+
const earlyIsTemporaryForbidden = err instanceof LLMAuthError
|
|
2962
|
+
&& err.statusCode === 403
|
|
2963
|
+
&& err.temporary === true;
|
|
2964
|
+
if (earlyIsTemporaryForbidden && canReplayProviderRequest
|
|
2965
|
+
&& consecutiveForbiddenErrors < retryPolicy.forbiddenRetryDelaysMs.length) {
|
|
2966
|
+
const delayMs = retryPolicy.forbiddenRetryDelaysMs[consecutiveForbiddenErrors];
|
|
2967
|
+
consecutiveForbiddenErrors += 1;
|
|
2968
|
+
endAttemptTrace('llm_retry');
|
|
2969
|
+
yield {
|
|
2970
|
+
type: 'llm_retry',
|
|
2971
|
+
attempt: consecutiveForbiddenErrors,
|
|
2972
|
+
maxRetries: retryPolicy.forbiddenRetryDelaysMs.length,
|
|
2973
|
+
delayMs,
|
|
2974
|
+
reason: 'temporary_forbidden',
|
|
2975
|
+
recoveryMode: 'restart',
|
|
2976
|
+
errorName: err.name,
|
|
2977
|
+
statusCode: 403,
|
|
2978
|
+
message: `LLM provider returned HTTP 403; retry ${consecutiveForbiddenErrors}/${retryPolicy.forbiddenRetryDelaysMs.length}`,
|
|
2979
|
+
};
|
|
2980
|
+
const slept = await sleepWithAbort(delayMs, signal);
|
|
2981
|
+
if (!slept || signal?.aborted) {
|
|
2982
|
+
yield { type: 'aborted', reason: this.#abortReason || 'external', turnNumber, threadId };
|
|
2983
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'aborted', threadId };
|
|
2984
|
+
break;
|
|
2985
|
+
}
|
|
2986
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'llm_retry', threadId };
|
|
2987
|
+
continue;
|
|
2988
|
+
}
|
|
2952
2989
|
if ((earlyIsRateLimit || earlyIsTransient) && canReplayProviderRequest) {
|
|
2953
2990
|
if (consecutiveRetryableErrors < retryPolicy.maxRetries) {
|
|
2954
2991
|
consecutiveRetryableErrors += 1;
|
package/yeaft/llm/adapter.js
CHANGED
|
@@ -74,15 +74,70 @@ export class LLMRateLimitError extends Error {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/** Authentication error (401, 403)
|
|
77
|
+
/** Authentication / authorization error (401, 403). */
|
|
78
78
|
export class LLMAuthError extends Error {
|
|
79
|
-
constructor(message, statusCode) {
|
|
79
|
+
constructor(message, statusCode, details = {}) {
|
|
80
80
|
super(message);
|
|
81
81
|
this.name = 'LLMAuthError';
|
|
82
82
|
this.statusCode = statusCode;
|
|
83
|
+
this.reasonCode = details.reasonCode || (statusCode === 401 ? 'invalid_credentials' : 'permission_denied');
|
|
84
|
+
this.temporary = details.temporary === true;
|
|
85
|
+
this.provider = details.provider || null;
|
|
86
|
+
this.model = details.model || null;
|
|
87
|
+
this.credentialRefreshable = details.credentialRefreshable === true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const PERMANENT_FORBIDDEN_RE = /(?:access[_ -]?denied|not[_ -]?authorized|unauthori[sz]ed|authorization[_ -]?denied|policy|safety|content[_ -]?filter|entitle(?:ment|d)|subscription[_ -]?(?:required|inactive|expired)|plan[_ -]?(?:required|does[_ -]?not[_ -]?include)|model[^\n]{0,60}(?:access|permission|unavailable|not[_ -]?available|plan)|permission[^\n]{0,40}model|account[^\n]{0,40}(?:disabled|suspended)|organization[^\n]{0,40}(?:disabled|suspended)|insufficient[_ -]?(?:permission|scope))/i;
|
|
92
|
+
const PERMANENT_FORBIDDEN_CODE_RE = /(?:access_denied|permission_denied|unauthorized|not_authorized|insufficient_(?:scope|permission)|model_(?:access_denied|not_available)|subscription_required|not_entitled|policy_violation|content_filter)/i;
|
|
93
|
+
|
|
94
|
+
function parseProviderErrorBody(responseBody) {
|
|
95
|
+
if (typeof responseBody !== 'string') return responseBody && typeof responseBody === 'object' ? responseBody : null;
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(responseBody);
|
|
98
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
83
101
|
}
|
|
84
102
|
}
|
|
85
103
|
|
|
104
|
+
function providerErrorSignals(responseBody) {
|
|
105
|
+
const parsed = parseProviderErrorBody(responseBody);
|
|
106
|
+
const error = parsed?.error && typeof parsed.error === 'object' ? parsed.error : parsed;
|
|
107
|
+
const code = [error?.code, error?.type, error?.reason, parsed?.code, parsed?.type]
|
|
108
|
+
.filter(value => typeof value === 'string')
|
|
109
|
+
.join(' ');
|
|
110
|
+
const message = [error?.message, parsed?.message, typeof responseBody === 'string' ? responseBody : '']
|
|
111
|
+
.filter(value => typeof value === 'string')
|
|
112
|
+
.join(' ');
|
|
113
|
+
return { code, message };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build a user-safe auth error. The provider response body is used only for
|
|
118
|
+
* classification and is deliberately excluded from the message; raw response
|
|
119
|
+
* capture remains available in the bounded debug trace.
|
|
120
|
+
*/
|
|
121
|
+
export function classifyAuthError(statusCode, responseBody = '', details = {}) {
|
|
122
|
+
const status = Number(statusCode) || 0;
|
|
123
|
+
if (status === 401) {
|
|
124
|
+
return new LLMAuthError('LLM provider returned HTTP 401 (invalid credentials)', status, {
|
|
125
|
+
...details,
|
|
126
|
+
reasonCode: 'invalid_credentials',
|
|
127
|
+
temporary: false,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const signals = providerErrorSignals(responseBody);
|
|
131
|
+
const permanent = PERMANENT_FORBIDDEN_CODE_RE.test(signals.code)
|
|
132
|
+
|| PERMANENT_FORBIDDEN_RE.test(signals.message);
|
|
133
|
+
const reasonCode = permanent ? 'permission_denied' : 'unknown_forbidden';
|
|
134
|
+
return new LLMAuthError(`LLM provider returned HTTP 403 (${reasonCode})`, status, {
|
|
135
|
+
...details,
|
|
136
|
+
reasonCode,
|
|
137
|
+
temporary: !permanent,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
86
141
|
/** Context too long error (413 or API-specific) — need compaction. */
|
|
87
142
|
export class LLMContextError extends Error {
|
|
88
143
|
constructor(message) {
|
package/yeaft/llm/anthropic.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
classifyFetchError,
|
|
13
13
|
retryAfterFromResponse,
|
|
14
14
|
LLMAuthError,
|
|
15
|
+
classifyAuthError,
|
|
15
16
|
LLMContextError,
|
|
16
17
|
LLMServerError,
|
|
17
18
|
LLMAbortError,
|
|
@@ -218,7 +219,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
218
219
|
#classifyError(status, body, response = null) {
|
|
219
220
|
const authHint = `auth=${this.#authHeaderMode}`;
|
|
220
221
|
if (status === 401 || status === 403) {
|
|
221
|
-
return
|
|
222
|
+
return classifyAuthError(status, body);
|
|
222
223
|
}
|
|
223
224
|
if (status === 429) {
|
|
224
225
|
const retryAfter = retryAfterFromResponse(response);
|
|
@@ -59,6 +59,16 @@ const JWT_REFRESH_MARGIN_SECONDS = 120;
|
|
|
59
59
|
const DEVICE_POLL_SAFETY_MARGIN_SECONDS = 3;
|
|
60
60
|
const DEFAULT_DEVICE_TIMEOUT_SECONDS = 300;
|
|
61
61
|
|
|
62
|
+
/** Safe credential exchange failure. Provider response bodies are never attached. */
|
|
63
|
+
export class CopilotCredentialError extends Error {
|
|
64
|
+
constructor(message, statusCode = null, reasonCode = 'credential_exchange_failed') {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = 'CopilotCredentialError';
|
|
67
|
+
this.statusCode = Number.isFinite(statusCode) ? statusCode : null;
|
|
68
|
+
this.reasonCode = reasonCode;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
const CREDENTIALS_DIR = join(homedir(), '.yeaft', 'credentials');
|
|
63
73
|
const CREDENTIALS_FILE = join(CREDENTIALS_DIR, 'github-copilot.json');
|
|
64
74
|
|
|
@@ -80,6 +90,11 @@ export function _resetCacheForTests() {
|
|
|
80
90
|
_exchangeInFlight.clear();
|
|
81
91
|
}
|
|
82
92
|
|
|
93
|
+
/** Drop exchanged API tokens after the provider rejects one with HTTP 401. */
|
|
94
|
+
export function invalidateApiTokenCache() {
|
|
95
|
+
_jwtCache.clear();
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
/**
|
|
84
99
|
* Anchor for `gh auth token` output validation. gh prints either a bare
|
|
85
100
|
* token followed by a newline or, on misconfiguration, a help banner /
|
|
@@ -270,12 +285,20 @@ export async function exchangeToken(rawToken, { fetchFn = fetch } = {}) {
|
|
|
270
285
|
});
|
|
271
286
|
|
|
272
287
|
if (!res.ok) {
|
|
273
|
-
|
|
274
|
-
|
|
288
|
+
throw new CopilotCredentialError(
|
|
289
|
+
`Copilot token exchange failed with HTTP ${res.status}`,
|
|
290
|
+
res.status,
|
|
291
|
+
);
|
|
275
292
|
}
|
|
276
293
|
const data = await res.json();
|
|
277
294
|
const apiToken = data && typeof data.token === 'string' ? data.token : '';
|
|
278
|
-
if (!apiToken)
|
|
295
|
+
if (!apiToken) {
|
|
296
|
+
throw new CopilotCredentialError(
|
|
297
|
+
'Copilot token exchange returned no API token',
|
|
298
|
+
res.status,
|
|
299
|
+
'credential_exchange_invalid_response',
|
|
300
|
+
);
|
|
301
|
+
}
|
|
279
302
|
|
|
280
303
|
const expiresAtRaw = data.expires_at;
|
|
281
304
|
const expiresAt =
|
|
@@ -307,13 +330,14 @@ export async function exchangeToken(rawToken, { fetchFn = fetch } = {}) {
|
|
|
307
330
|
* @param {typeof fetch} [opts.fetchFn]
|
|
308
331
|
* @returns {Promise<{token: string, source: string, exchanged: boolean} | null>}
|
|
309
332
|
*/
|
|
310
|
-
export async function getApiToken({ hostname, fetchFn = fetch } = {}) {
|
|
333
|
+
export async function getApiToken({ hostname, fetchFn = fetch, requireExchange = false } = {}) {
|
|
311
334
|
const raw = await resolveRawToken({ hostname });
|
|
312
335
|
if (!raw) return null;
|
|
313
336
|
try {
|
|
314
337
|
const { apiToken } = await exchangeToken(raw.token, { fetchFn });
|
|
315
338
|
return { token: apiToken, source: raw.source, exchanged: true };
|
|
316
|
-
} catch {
|
|
339
|
+
} catch (err) {
|
|
340
|
+
if (requireExchange) throw err;
|
|
317
341
|
return { token: raw.token, source: raw.source, exchanged: false };
|
|
318
342
|
}
|
|
319
343
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import * as githubCopilot from './github-copilot.js';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* @typedef {{ getApiKey: () => Promise<string>, name: string }} CredentialProvider
|
|
23
|
+
* @typedef {{ getApiKey: () => Promise<string>, refreshApiKey?: () => Promise<string>, name: string }} CredentialProvider
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
/**
|
|
@@ -42,6 +42,18 @@ export function getCredentialProvider(name) {
|
|
|
42
42
|
}
|
|
43
43
|
return r.token;
|
|
44
44
|
},
|
|
45
|
+
async refreshApiKey() {
|
|
46
|
+
githubCopilot.invalidateApiTokenCache();
|
|
47
|
+
const r = await githubCopilot.getApiToken({ requireExchange: true });
|
|
48
|
+
if (!r?.token) {
|
|
49
|
+
throw new githubCopilot.CopilotCredentialError(
|
|
50
|
+
'GitHub Copilot credential refresh found no usable credential',
|
|
51
|
+
null,
|
|
52
|
+
'credential_unavailable',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return r.token;
|
|
56
|
+
},
|
|
45
57
|
};
|
|
46
58
|
}
|
|
47
59
|
return null;
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
LLMAdapter,
|
|
30
30
|
LLMRateLimitError,
|
|
31
31
|
LLMAuthError,
|
|
32
|
+
classifyAuthError,
|
|
32
33
|
LLMContextError,
|
|
33
34
|
LLMServerError,
|
|
34
35
|
LLMAbortError,
|
|
@@ -207,7 +208,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
207
208
|
|
|
208
209
|
#classifyError(status, body, response = null) {
|
|
209
210
|
if (status === 401 || status === 403) {
|
|
210
|
-
return
|
|
211
|
+
return classifyAuthError(status, body);
|
|
211
212
|
}
|
|
212
213
|
if (status === 429) {
|
|
213
214
|
const retryAfter = retryAfterFromResponse(response);
|
package/yeaft/llm/router.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Phase 7 removed the legacy "openai" (Chat Completions) protocol entirely.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { LLMAdapter } from './adapter.js';
|
|
24
|
+
import { LLMAdapter, LLMAuthError } from './adapter.js';
|
|
25
25
|
import { getModelEffortOptions, getThinkingCapability, normalizeEffort, normalizeEffortOptions, parseModelRef } from '../models.js';
|
|
26
26
|
import {
|
|
27
27
|
GITHUB_COPILOT_BASE_URL,
|
|
@@ -563,18 +563,62 @@ export class AdapterRouter extends LLMAdapter {
|
|
|
563
563
|
* @param {import('./adapter.js').StreamParams} params
|
|
564
564
|
* @returns {AsyncGenerator<import('./adapter.js').StreamEvent>}
|
|
565
565
|
*/
|
|
566
|
+
async #refreshRejectedCredential(provider, model) {
|
|
567
|
+
if (!provider?.credentialProvider) return false;
|
|
568
|
+
try {
|
|
569
|
+
if (provider.credentialProvider === 'github-copilot' && provider.githubToken) {
|
|
570
|
+
const githubCopilot = await import('./credentials/github-copilot.js');
|
|
571
|
+
githubCopilot.invalidateApiTokenCache();
|
|
572
|
+
await githubCopilot.exchangeToken(provider.githubToken);
|
|
573
|
+
} else {
|
|
574
|
+
const { getCredentialProvider } = await import('./credentials/index.js');
|
|
575
|
+
const credentialProvider = getCredentialProvider(provider.credentialProvider);
|
|
576
|
+
if (!credentialProvider?.refreshApiKey) return false;
|
|
577
|
+
await credentialProvider.refreshApiKey();
|
|
578
|
+
}
|
|
579
|
+
} catch (err) {
|
|
580
|
+
throw new LLMAuthError('LLM credential refresh failed', err?.statusCode ?? 401, {
|
|
581
|
+
reasonCode: err?.reasonCode || 'credential_refresh_failed',
|
|
582
|
+
provider: provider.name || null,
|
|
583
|
+
model,
|
|
584
|
+
credentialRefreshable: true,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
this.#adapterCache.clear();
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
#annotateAuthError(err, provider, model) {
|
|
592
|
+
if (err?.name !== 'LLMAuthError') return;
|
|
593
|
+
err.provider = provider?.name || null;
|
|
594
|
+
err.model = model;
|
|
595
|
+
err.credentialRefreshable = Boolean(provider?.credentialProvider);
|
|
596
|
+
}
|
|
597
|
+
|
|
566
598
|
async *stream(params) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
599
|
+
let refreshedCredential = false;
|
|
600
|
+
while (true) {
|
|
601
|
+
const resolved = await this.#resolveAdapter(params.model);
|
|
602
|
+
const provider = this.getProviderForModel(params.model);
|
|
603
|
+
const effortContext = {
|
|
604
|
+
protocol: resolved.protocol,
|
|
605
|
+
supportsEffort: resolved.entry?.supportsEffort,
|
|
606
|
+
effortOptions: resolved.entry?.effortOptions,
|
|
607
|
+
thinkingProtocol: resolved.entry?.thinkingProtocol,
|
|
608
|
+
maxBudgetTokens: resolved.entry?.maxBudgetTokens,
|
|
609
|
+
};
|
|
610
|
+
const filtered = filterEffortForModel({ ...params, model: resolved.modelId }, resolved);
|
|
611
|
+
const sanitized = sanitizeMessagesForWire(filtered);
|
|
612
|
+
try {
|
|
613
|
+
yield* resolved.adapter.stream({ ...sanitized, model: resolved.modelId, effortContext });
|
|
614
|
+
return;
|
|
615
|
+
} catch (err) {
|
|
616
|
+
this.#annotateAuthError(err, provider, params.model);
|
|
617
|
+
if (err?.statusCode !== 401 || refreshedCredential
|
|
618
|
+
|| !(await this.#refreshRejectedCredential(provider, params.model))) throw err;
|
|
619
|
+
refreshedCredential = true;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
578
622
|
}
|
|
579
623
|
|
|
580
624
|
/**
|
|
@@ -584,17 +628,28 @@ export class AdapterRouter extends LLMAdapter {
|
|
|
584
628
|
* @returns {Promise<{ text: string, usage: { inputTokens: number, outputTokens: number } }>}
|
|
585
629
|
*/
|
|
586
630
|
async call(params) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
631
|
+
let refreshedCredential = false;
|
|
632
|
+
while (true) {
|
|
633
|
+
const resolved = await this.#resolveAdapter(params.model);
|
|
634
|
+
const provider = this.getProviderForModel(params.model);
|
|
635
|
+
const effortContext = {
|
|
636
|
+
protocol: resolved.protocol,
|
|
637
|
+
supportsEffort: resolved.entry?.supportsEffort,
|
|
638
|
+
effortOptions: resolved.entry?.effortOptions,
|
|
639
|
+
thinkingProtocol: resolved.entry?.thinkingProtocol,
|
|
640
|
+
maxBudgetTokens: resolved.entry?.maxBudgetTokens,
|
|
641
|
+
};
|
|
642
|
+
const filtered = filterEffortForModel({ ...params, model: resolved.modelId }, resolved);
|
|
643
|
+
const sanitized = sanitizeMessagesForWire(filtered);
|
|
644
|
+
try {
|
|
645
|
+
return await resolved.adapter.call({ ...sanitized, model: resolved.modelId, effortContext });
|
|
646
|
+
} catch (err) {
|
|
647
|
+
this.#annotateAuthError(err, provider, params.model);
|
|
648
|
+
if (err?.statusCode !== 401 || refreshedCredential
|
|
649
|
+
|| !(await this.#refreshRejectedCredential(provider, params.model))) throw err;
|
|
650
|
+
refreshedCredential = true;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
598
653
|
}
|
|
599
654
|
|
|
600
655
|
/**
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -3778,7 +3778,8 @@ function handleEngineEvent(event, hctx) {
|
|
|
3778
3778
|
const terminalTurnEnd = event.type === 'turn_end' && event.terminal === true;
|
|
3779
3779
|
const managesQueryTimer = terminalTurnEnd
|
|
3780
3780
|
|| event.type === 'async_task_wait_start'
|
|
3781
|
-
|| event.type === 'async_task_wait_end'
|
|
3781
|
+
|| event.type === 'async_task_wait_end'
|
|
3782
|
+
|| event.type === 'llm_retry';
|
|
3782
3783
|
if (!managesQueryTimer) hctx.resetQueryTimer();
|
|
3783
3784
|
const envelope = {
|
|
3784
3785
|
sessionId: hctx.sessionId,
|
|
@@ -4074,6 +4075,9 @@ function handleEngineEvent(event, hctx) {
|
|
|
4074
4075
|
break;
|
|
4075
4076
|
|
|
4076
4077
|
case 'llm_retry':
|
|
4078
|
+
// Engine is intentionally sleeping before the next provider request.
|
|
4079
|
+
// Silence watchdogs protect active calls, not declared retry waits.
|
|
4080
|
+
if (typeof hctx.pauseQueryTimer === 'function') hctx.pauseQueryTimer();
|
|
4077
4081
|
maybeTransitionVpStatus(hctx, 'retrying');
|
|
4078
4082
|
// Engine paused before re-issuing the same turn because the LLM
|
|
4079
4083
|
// returned a retryable error (rate limit / 5xx / transient network /
|
|
@@ -4284,6 +4288,11 @@ function handleEngineEvent(event, hctx) {
|
|
|
4284
4288
|
type: 'error',
|
|
4285
4289
|
message: visibleErrMsg,
|
|
4286
4290
|
errorName: event.error?.name || null,
|
|
4291
|
+
statusCode: event.error?.statusCode ?? null,
|
|
4292
|
+
reasonCode: event.error?.reasonCode || null,
|
|
4293
|
+
provider: event.error?.provider || null,
|
|
4294
|
+
model: event.error?.model || null,
|
|
4295
|
+
credentialRefreshable: event.error?.credentialRefreshable === true,
|
|
4287
4296
|
retryable: !!event.retryable,
|
|
4288
4297
|
...(event.reason ? { reason: event.reason } : {}),
|
|
4289
4298
|
...(event.retryExhausted !== undefined ? { retryExhausted: !!event.retryExhausted } : {}),
|