feat(kiro): 添加认证错误自动标记不健康及凭证切换重试机制,解决401 和403的问题

1. Kiro API 新增 401/403 错误处理逻辑,401 尝试刷新 token 后重试,403 直接标记凭证不健康
2. 新增 _markCredentialUnhealthy 辅助方法,统一处理凭证健康状态标记
3. ProviderPoolManager 新增 markProviderUnhealthyImmediately 方法,用于认证错误立即标记
4. handleStreamRequest/handleUnaryRequest 支持重试上下文,认证失败后自动切换健康凭证重试
5. provider-api 健康检查增加认证错误识别,触发立即标记不健康逻辑
This commit is contained in:
leonai 2026-01-13 18:41:22 +08:00
parent b1c9be7724
commit 948283187c
4 changed files with 304 additions and 38 deletions

View file

@ -9,7 +9,8 @@ import * as https from 'https';
import { getProviderModels } from '../provider-models.js';
import { countTokens } from '@anthropic-ai/tokenizer';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { isRetryableNetworkError } from '../../utils/common.js';
import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js';
import { getProviderPoolManager } from '../../services/service-manager.js';
const KIRO_THINKING = {
MAX_BUDGET_TOKENS: 24576,
@ -1212,16 +1213,27 @@ async initializeAuth(forceRefresh = false) {
// 检查是否为可重试的网络错误
const isNetworkError = isRetryableNetworkError(error);
if (status === 403 && !isRetry) {
console.log('[Kiro] Received 403. Attempting token refresh and retrying...');
// Handle 401 (Unauthorized) - try to refresh token first
if (status === 401 && !isRetry) {
console.log('[Kiro] Received 401. Attempting token refresh...');
try {
await this.initializeAuth(true); // Force refresh token
console.log('[Kiro] Token refresh successful after 401, retrying request...');
return this.callApi(method, model, body, true, retryCount);
} catch (refreshError) {
console.error('[Kiro] Token refresh failed during 403 retry:', refreshError.message);
console.error('[Kiro] Token refresh failed during 401 retry:', refreshError.message);
// Mark credential as unhealthy immediately and attach marker to error
this._markCredentialUnhealthy('401 Unauthorized - Token refresh failed', refreshError);
throw refreshError;
}
}
// Handle 403 (Forbidden) - mark as unhealthy immediately, no retry
if (status === 403) {
console.log('[Kiro] Received 403. Marking credential as unhealthy...');
this._markCredentialUnhealthy('403 Forbidden', error);
throw error;
}
// Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) {
@ -1253,6 +1265,31 @@ async initializeAuth(forceRefresh = false) {
}
}
/**
* Helper method to mark the current credential as unhealthy
* @param {string} reason - The reason for marking unhealthy
* @param {Error} [error] - Optional error object to attach the marker to
* @returns {boolean} - Whether the credential was successfully marked as unhealthy
* @private
*/
_markCredentialUnhealthy(reason, error = null) {
const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) {
console.log(`[Kiro] Marking credential ${this.uuid} as unhealthy. Reason: ${reason}`);
poolManager.markProviderUnhealthyImmediately(MODEL_PROVIDER.KIRO_API, {
uuid: this.uuid
}, reason);
// Attach marker to error object to prevent duplicate marking in upper layers
if (error) {
error.credentialMarkedUnhealthy = true;
}
return true;
} else {
console.warn(`[Kiro] Cannot mark credential as unhealthy: poolManager=${!!poolManager}, uuid=${this.uuid}`);
return false;
}
}
_processApiResponse(response) {
const rawResponseText = Buffer.isBuffer(response.data) ? response.data.toString('utf8') : String(response.data);
//console.log(`[Kiro] Raw response length: ${rawResponseText.length}`);
@ -1537,11 +1574,27 @@ async initializeAuth(forceRefresh = false) {
// 检查是否为可重试的网络错误
const isNetworkError = isRetryableNetworkError(error);
if (status === 403 && !isRetry) {
console.log('[Kiro] Received 403 in stream. Attempting token refresh and retrying...');
await this.initializeAuth(true);
yield* this.streamApiReal(method, model, body, true, retryCount);
return;
// Handle 401 (Unauthorized) - try to refresh token first
if (status === 401 && !isRetry) {
console.log('[Kiro] Received 401 in stream. Attempting token refresh...');
try {
await this.initializeAuth(true); // Force refresh token
console.log('[Kiro] Token refresh successful after 401, retrying stream...');
yield* this.streamApiReal(method, model, body, true, retryCount);
return;
} catch (refreshError) {
console.error('[Kiro] Token refresh failed during 401 retry:', refreshError.message);
// Mark credential as unhealthy immediately and attach marker to error
this._markCredentialUnhealthy('401 Unauthorized - Token refresh failed', refreshError);
throw refreshError;
}
}
// Handle 403 (Forbidden) - mark as unhealthy immediately, no retry
if (status === 403) {
console.log('[Kiro] Received 403 in stream. Marking credential as unhealthy...');
this._markCredentialUnhealthy('403 Forbidden', error);
throw error;
}
if (status === 429 && retryCount < maxRetries) {
@ -2355,24 +2408,42 @@ async initializeAuth(forceRefresh = false) {
console.log('[Kiro] Usage limits fetched successfully');
return response.data;
} catch (error) {
// 如果是 403 错误,尝试刷新 token 后重试
if (error.response?.status === 403) {
console.log('[Kiro] Received 403 on getUsageLimits. Attempting token refresh and retrying...');
try {
await this.initializeAuth(true);
// 更新 Authorization header
headers['Authorization'] = `Bearer ${this.accessToken}`;
headers['amz-sdk-invocation-id'] = uuidv4();
const retryResponse = await this.axiosInstance.get(fullUrl, { headers });
console.log('[Kiro] Usage limits fetched successfully after token refresh');
return retryResponse.data;
} catch (refreshError) {
console.error('[Kiro] Token refresh failed during getUsageLimits retry:', refreshError.message);
throw refreshError;
const status = error.response?.status;
// 从响应体中提取错误信息
let errorMessage = error.message;
if (error.response?.data) {
// 尝试从响应体中获取错误描述
const responseData = error.response.data;
if (typeof responseData === 'string') {
errorMessage = responseData;
} else if (responseData.message) {
errorMessage = responseData.message;
} else if (responseData.error) {
errorMessage = typeof responseData.error === 'string' ? responseData.error : responseData.error.message || JSON.stringify(responseData.error);
}
}
console.error('[Kiro] Failed to fetch usage limits:', error.message, error);
throw error;
// 构建包含状态码和错误描述的错误信息
const formattedError = status
? new Error(`API call failed: ${status} - ${errorMessage}`)
: new Error(`API call failed: ${errorMessage}`);
// 对于用量查询401/403 错误直接标记凭证为不健康,不重试
if (status === 401) {
console.log('[Kiro] Received 401 on getUsageLimits. Marking credential as unhealthy (no retry)...');
this._markCredentialUnhealthy('401 Unauthorized on usage query', formattedError);
throw formattedError;
}
if (status === 403) {
console.log('[Kiro] Received 403 on getUsageLimits. Marking credential as unhealthy (no retry)...');
this._markCredentialUnhealthy('403 Forbidden on usage query', formattedError);
throw formattedError;
}
console.error('[Kiro] Failed to fetch usage limits:', formattedError.message, error);
throw formattedError;
}
}
}

View file

@ -429,6 +429,35 @@ export class ProviderPoolManager {
}
}
/**
* Marks a provider as unhealthy immediately (without accumulating error count).
* Used for definitive authentication errors like 401/403.
* @param {string} providerType - The type of the provider.
* @param {object} providerConfig - The configuration of the provider to mark.
* @param {string} [errorMessage] - Optional error message to store.
*/
markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage = null) {
if (!providerConfig?.uuid) {
this._log('error', 'Invalid providerConfig in markProviderUnhealthyImmediately');
return;
}
const provider = this._findProvider(providerType, providerConfig.uuid);
if (provider) {
provider.config.isHealthy = false;
provider.config.errorCount = this.maxErrorCount; // Set to max to indicate definitive failure
provider.config.lastErrorTime = new Date().toISOString();
provider.config.lastUsed = new Date().toISOString();
if (errorMessage) {
provider.config.lastErrorMessage = errorMessage;
}
this._log('warn', `Immediately marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Reason: ${errorMessage || 'Authentication error'}`);
this._debouncedSave(providerType);
}
}
/**
* Marks a provider as healthy.
* @param {string} providerType - The type of the provider.

View file

@ -521,7 +521,18 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
message: 'Healthy'
});
} else {
providerPoolManager.markProviderUnhealthy(providerType, providerConfig, healthResult.errorMessage);
// 检查是否为认证错误401/403如果是则立即标记为不健康
const errorMessage = healthResult.errorMessage || 'Check failed';
const isAuthError = /\b(401|403)\b/.test(errorMessage) ||
/\b(Unauthorized|Forbidden|AccessDenied|InvalidToken|ExpiredToken)\b/i.test(errorMessage);
if (isAuthError) {
providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage);
console.log(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`);
} else {
providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage);
}
providerStatus.config.lastHealthCheckTime = new Date().toISOString();
if (healthResult.modelName) {
providerStatus.config.lastHealthCheckModel = healthResult.modelName;
@ -530,15 +541,28 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
uuid: providerConfig.uuid,
success: false,
modelName: healthResult.modelName,
message: healthResult.errorMessage || 'Check failed'
message: errorMessage,
isAuthError: isAuthError
});
}
} catch (error) {
providerPoolManager.markProviderUnhealthy(providerType, providerConfig, error.message);
const errorMessage = error.message || 'Unknown error';
// 检查是否为认证错误401/403如果是则立即标记为不健康
const isAuthError = /\b(401|403)\b/.test(errorMessage) ||
/\b(Unauthorized|Forbidden|AccessDenied|InvalidToken|ExpiredToken)\b/i.test(errorMessage);
if (isAuthError) {
providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage);
console.log(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`);
} else {
providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage);
}
results.push({
uuid: providerConfig.uuid,
success: false,
message: error.message
message: errorMessage,
isAuthError: isAuthError
});
}
}

View file

@ -217,13 +217,22 @@ export async function handleUnifiedResponse(res, responsePayload, isStream) {
}
}
export async function handleStreamRequest(res, service, model, requestBody, fromProvider, toProvider, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid, customName) {
export async function handleStreamRequest(res, service, model, requestBody, fromProvider, toProvider, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid, customName, retryContext = null) {
let fullResponseText = '';
let fullResponseJson = '';
let fullOldResponseJson = '';
let responseClosed = false;
// 重试上下文:包含 CONFIG 和重试计数
const maxRetries = retryContext?.maxRetries ?? 2;
const currentRetry = retryContext?.currentRetry ?? 0;
const CONFIG = retryContext?.CONFIG;
const isRetry = currentRetry > 0;
await handleUnifiedResponse(res, '', true);
// 只在首次请求时发送响应头,重试时跳过(响应头已发送)
if (!isRetry) {
await handleUnifiedResponse(res, '', true);
}
// fs.writeFile('request'+Date.now()+'.json', JSON.stringify(requestBody));
// The service returns a stream in its native format (toProvider).
@ -283,12 +292,80 @@ export async function handleStreamRequest(res, service, model, requestBody, from
} catch (error) {
console.error('\n[Server] Error during stream processing:', error.stack);
if (providerPoolManager && pooluuid) {
console.log(`[Provider Pool] Marking ${toProvider} as unhealthy due to stream error`);
// 如果已经发送了内容,不进行重试(避免响应数据损坏)
if (fullResponseText.length > 0) {
console.log(`[Stream Retry] Cannot retry: ${fullResponseText.length} bytes already sent to client`);
// 直接发送错误并结束
const errorPayload = createStreamErrorResponse(error, fromProvider);
res.write(errorPayload);
res.end();
responseClosed = true;
return;
}
// 获取状态码(用于日志记录,不再用于判断是否重试)
const status = error.response?.status;
// 检查凭证是否已在底层被标记为不健康(避免重复标记)
let credentialMarkedUnhealthy = error.credentialMarkedUnhealthy === true;
// 如果底层未标记,则在此处标记
if (!credentialMarkedUnhealthy && providerPoolManager && pooluuid) {
console.log(`[Provider Pool] Marking ${toProvider} as unhealthy due to stream error (status: ${status || 'unknown'})`);
// 如果是号池模式,并且请求处理失败,则标记当前使用的提供者为不健康
providerPoolManager.markProviderUnhealthy(toProvider, {
uuid: pooluuid
});
credentialMarkedUnhealthy = true;
} else if (credentialMarkedUnhealthy) {
console.log(`[Provider Pool] Credential ${pooluuid} already marked as unhealthy by lower layer, skipping duplicate marking`);
}
// 凭证已被标记为不健康后,尝试切换到新凭证重试
// 不再依赖状态码判断,只要凭证被标记不健康且可以重试,就尝试切换
if (credentialMarkedUnhealthy && currentRetry < maxRetries && providerPoolManager && CONFIG) {
console.log(`[Stream Retry] Credential marked unhealthy. Attempting retry ${currentRetry + 1}/${maxRetries} with different credential...`);
try {
// 动态导入以避免循环依赖
const { getApiServiceWithFallback } = await import('../services/service-manager.js');
const result = await getApiServiceWithFallback(CONFIG, model);
if (result && result.service && result.uuid !== pooluuid) {
console.log(`[Stream Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`);
// 使用新服务重试
const newRetryContext = {
...retryContext,
CONFIG,
currentRetry: currentRetry + 1,
maxRetries
};
// 递归调用,使用新的服务
return await handleStreamRequest(
res,
result.service,
result.actualModel || model,
requestBody,
fromProvider,
result.actualProviderType || toProvider,
PROMPT_LOG_MODE,
PROMPT_LOG_FILENAME,
providerPoolManager,
result.uuid,
result.serviceConfig?.customName || customName,
newRetryContext
);
} else if (result && result.uuid === pooluuid) {
console.log(`[Stream Retry] No different healthy credential available. Same credential selected.`);
} else {
console.log(`[Stream Retry] No healthy credential available for retry.`);
}
} catch (retryError) {
console.error(`[Stream Retry] Failed to get alternative service:`, retryError.message);
}
}
// 使用新方法创建符合 fromProvider 格式的流式错误响应
@ -307,7 +384,12 @@ export async function handleStreamRequest(res, service, model, requestBody, from
}
export async function handleUnaryRequest(res, service, model, requestBody, fromProvider, toProvider, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid, customName) {
export async function handleUnaryRequest(res, service, model, requestBody, fromProvider, toProvider, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid, customName, retryContext = null) {
// 重试上下文:包含 CONFIG 和重试计数
const maxRetries = retryContext?.maxRetries ?? 2;
const currentRetry = retryContext?.currentRetry ?? 0;
const CONFIG = retryContext?.CONFIG;
try{
// The service returns the response in its native format (toProvider).
const needsConversion = getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider);
@ -338,12 +420,69 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
}
} catch (error) {
console.error('\n[Server] Error during unary processing:', error.stack);
if (providerPoolManager && pooluuid) {
console.log(`[Provider Pool] Marking ${toProvider} as unhealthy due to stream error`);
// 获取状态码(用于日志记录,不再用于判断是否重试)
const status = error.response?.status;
// 检查凭证是否已在底层被标记为不健康(避免重复标记)
let credentialMarkedUnhealthy = error.credentialMarkedUnhealthy === true;
// 如果底层未标记,则在此处标记
if (!credentialMarkedUnhealthy && providerPoolManager && pooluuid) {
console.log(`[Provider Pool] Marking ${toProvider} as unhealthy due to unary error (status: ${status || 'unknown'})`);
// 如果是号池模式,并且请求处理失败,则标记当前使用的提供者为不健康
providerPoolManager.markProviderUnhealthy(toProvider, {
uuid: pooluuid
});
credentialMarkedUnhealthy = true;
} else if (credentialMarkedUnhealthy) {
console.log(`[Provider Pool] Credential ${pooluuid} already marked as unhealthy by lower layer, skipping duplicate marking`);
}
// 凭证已被标记为不健康后,尝试切换到新凭证重试
// 不再依赖状态码判断,只要凭证被标记不健康且可以重试,就尝试切换
if (credentialMarkedUnhealthy && currentRetry < maxRetries && providerPoolManager && CONFIG) {
console.log(`[Unary Retry] Credential marked unhealthy. Attempting retry ${currentRetry + 1}/${maxRetries} with different credential...`);
try {
// 动态导入以避免循环依赖
const { getApiServiceWithFallback } = await import('../services/service-manager.js');
const result = await getApiServiceWithFallback(CONFIG, model);
if (result && result.service && result.uuid !== pooluuid) {
console.log(`[Unary Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`);
// 使用新服务重试
const newRetryContext = {
...retryContext,
CONFIG,
currentRetry: currentRetry + 1,
maxRetries
};
// 递归调用,使用新的服务
return await handleUnaryRequest(
res,
result.service,
result.actualModel || model,
requestBody,
fromProvider,
result.actualProviderType || toProvider,
PROMPT_LOG_MODE,
PROMPT_LOG_FILENAME,
providerPoolManager,
result.uuid,
result.serviceConfig?.customName || customName,
newRetryContext
);
} else if (result && result.uuid === pooluuid) {
console.log(`[Unary Retry] No different healthy credential available. Same credential selected.`);
} else {
console.log(`[Unary Retry] No healthy credential available for retry.`);
}
} catch (retryError) {
console.error(`[Unary Retry] Failed to get alternative service:`, retryError.message);
}
}
// 使用新方法创建符合 fromProvider 格式的错误响应
@ -487,10 +626,13 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
await logConversation('input', promptText, CONFIG.PROMPT_LOG_MODE, PROMPT_LOG_FILENAME);
// 5. Call the appropriate stream or unary handler, passing the provider info.
// 创建重试上下文,包含 CONFIG 以便在认证错误时切换凭证重试
const retryContext = providerPoolManager ? { CONFIG, currentRetry: 0, maxRetries: 2 } : null;
if (isStream) {
await handleStreamRequest(res, service, model, processedRequestBody, fromProvider, toProvider, CONFIG.PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, actualUuid, actualCustomName);
await handleStreamRequest(res, service, model, processedRequestBody, fromProvider, toProvider, CONFIG.PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, actualUuid, actualCustomName, retryContext);
} else {
await handleUnaryRequest(res, service, model, processedRequestBody, fromProvider, toProvider, CONFIG.PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, actualUuid, actualCustomName);
await handleUnaryRequest(res, service, model, processedRequestBody, fromProvider, toProvider, CONFIG.PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, actualUuid, actualCustomName, retryContext);
}
// 执行插件钩子:内容生成后