Merge pull request #233 from leonaii/main
fix(kiro): 增强 Kiro Provider 的认证恢复机制和并发处理能力
This commit is contained in:
commit
d1c8088a0c
3 changed files with 220 additions and 27 deletions
|
|
@ -61,6 +61,10 @@ const MODEL_MAPPING = Object.fromEntries(
|
||||||
|
|
||||||
const KIRO_AUTH_TOKEN_FILE = "kiro-auth-token.json";
|
const KIRO_AUTH_TOKEN_FILE = "kiro-auth-token.json";
|
||||||
|
|
||||||
|
// Token 刷新单例锁 - 按凭证文件路径索引,防止多个并发请求同时刷新同一个 token
|
||||||
|
// 这解决了文件锁导致的并发请求串行化问题
|
||||||
|
const tokenRefreshPromises = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kiro API Service - Node.js implementation based on the Python ki2api
|
* Kiro API Service - Node.js implementation based on the Python ki2api
|
||||||
* Provides OpenAI-compatible API for Claude Sonnet 4 via Kiro/CodeWhisperer
|
* Provides OpenAI-compatible API for Claude Sonnet 4 via Kiro/CodeWhisperer
|
||||||
|
|
@ -410,16 +414,45 @@ async initializeAuth(forceRefresh = false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取凭证文件路径,用于单例锁的 key
|
||||||
|
const tokenFilePath = this.credsFilePath || path.join(this.credPath, KIRO_AUTH_TOKEN_FILE);
|
||||||
|
|
||||||
|
// 单例刷新逻辑:如果已有刷新在进行中,等待它完成而不是重复刷新
|
||||||
|
if (forceRefresh && tokenRefreshPromises.has(tokenFilePath)) {
|
||||||
|
console.log('[Kiro Auth] Token refresh already in progress for this credential, waiting...');
|
||||||
|
try {
|
||||||
|
await tokenRefreshPromises.get(tokenFilePath);
|
||||||
|
// 刷新完成后,重新加载凭证(因为其他请求可能已经更新了 token)
|
||||||
|
await this._reloadCredentialsAfterRefresh(tokenFilePath);
|
||||||
|
console.log('[Kiro Auth] Reused token from concurrent refresh');
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
// 如果等待的刷新失败了,我们需要自己尝试刷新
|
||||||
|
console.warn('[Kiro Auth] Concurrent refresh failed, will attempt own refresh:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to load credentials from a file
|
// Helper to load credentials from a file
|
||||||
const loadCredentialsFromFile = async (filePath) => {
|
const loadCredentialsFromFile = async (filePath) => {
|
||||||
try {
|
try {
|
||||||
const fileContent = await fs.readFile(filePath, 'utf8');
|
const fileContent = await fs.readFile(filePath, 'utf8');
|
||||||
return JSON.parse(fileContent);
|
try {
|
||||||
|
return JSON.parse(fileContent);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('[Kiro Auth] JSON parse failed, attempting repair...');
|
||||||
|
try {
|
||||||
|
const repaired = repairJson(fileContent);
|
||||||
|
const result = JSON.parse(repaired);
|
||||||
|
console.info('[Kiro Auth] JSON repair successful');
|
||||||
|
return result;
|
||||||
|
} catch (repairError) {
|
||||||
|
console.error('[Kiro Auth] JSON repair failed:', repairError.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') {
|
if (error.code === 'ENOENT') {
|
||||||
console.debug(`[Kiro Auth] Credential file not found: ${filePath}`);
|
console.debug(`[Kiro Auth] Credential file not found: ${filePath}`);
|
||||||
} else if (error instanceof SyntaxError) {
|
|
||||||
console.warn(`[Kiro Auth] Failed to parse JSON from ${filePath}: ${error.message}`);
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[Kiro Auth] Failed to read credential file ${filePath}: ${error.message}`);
|
console.warn(`[Kiro Auth] Failed to read credential file ${filePath}: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
@ -435,7 +468,19 @@ async initializeAuth(forceRefresh = false) {
|
||||||
let existingData = {};
|
let existingData = {};
|
||||||
try {
|
try {
|
||||||
const fileContent = await fs.readFile(filePath, 'utf8');
|
const fileContent = await fs.readFile(filePath, 'utf8');
|
||||||
existingData = JSON.parse(fileContent);
|
try {
|
||||||
|
existingData = JSON.parse(fileContent);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('[Kiro Auth] JSON parse failed, attempting repair...');
|
||||||
|
try {
|
||||||
|
const repaired = repairJson(fileContent);
|
||||||
|
existingData = JSON.parse(repaired);
|
||||||
|
console.info('[Kiro Auth] JSON repair successful');
|
||||||
|
} catch (repairError) {
|
||||||
|
console.error('[Kiro Auth] JSON repair failed:', repairError.message);
|
||||||
|
existingData = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (readError) {
|
} catch (readError) {
|
||||||
if (readError.code === 'ENOENT') {
|
if (readError.code === 'ENOENT') {
|
||||||
console.debug(`[Kiro Auth] Token file not found, creating new one: ${filePath}`);
|
console.debug(`[Kiro Auth] Token file not found, creating new one: ${filePath}`);
|
||||||
|
|
@ -529,6 +574,30 @@ async initializeAuth(forceRefresh = false) {
|
||||||
if (!this.refreshToken) {
|
if (!this.refreshToken) {
|
||||||
throw new Error('No refresh token available to refresh access token.');
|
throw new Error('No refresh token available to refresh access token.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建刷新 Promise 并存入单例锁 Map
|
||||||
|
const refreshPromise = this._doTokenRefresh(saveCredentialsToFile, tokenFilePath);
|
||||||
|
tokenRefreshPromises.set(tokenFilePath, refreshPromise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await refreshPromise;
|
||||||
|
} finally {
|
||||||
|
// 刷新完成后清理单例锁
|
||||||
|
tokenRefreshPromises.delete(tokenFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.accessToken) {
|
||||||
|
throw new Error('No access token available after initialization and refresh attempts.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行实际的 token 刷新操作(内部方法)
|
||||||
|
* @param {Function} saveCredentialsToFile - 保存凭证的函数
|
||||||
|
* @param {string} tokenFilePath - 凭证文件路径
|
||||||
|
*/
|
||||||
|
async _doTokenRefresh(saveCredentialsToFile, tokenFilePath) {
|
||||||
try {
|
try {
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
refreshToken: this.refreshToken,
|
refreshToken: this.refreshToken,
|
||||||
|
|
@ -546,7 +615,7 @@ async initializeAuth(forceRefresh = false) {
|
||||||
if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) {
|
if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) {
|
||||||
response = await this.axiosSocialRefreshInstance.post(refreshUrl, requestBody);
|
response = await this.axiosSocialRefreshInstance.post(refreshUrl, requestBody);
|
||||||
console.log('[Kiro Auth] Token refresh social response: ok');
|
console.log('[Kiro Auth] Token refresh social response: ok');
|
||||||
}else{
|
} else {
|
||||||
response = await this.axiosInstance.post(refreshUrl, requestBody);
|
response = await this.axiosInstance.post(refreshUrl, requestBody);
|
||||||
console.log('[Kiro Auth] Token refresh idc response: ok');
|
console.log('[Kiro Auth] Token refresh idc response: ok');
|
||||||
}
|
}
|
||||||
|
|
@ -560,14 +629,12 @@ async initializeAuth(forceRefresh = false) {
|
||||||
this.expiresAt = expiresAt;
|
this.expiresAt = expiresAt;
|
||||||
console.info('[Kiro Auth] Access token refreshed successfully');
|
console.info('[Kiro Auth] Access token refreshed successfully');
|
||||||
|
|
||||||
// Update the token file - use specified path if configured, otherwise use default
|
|
||||||
const tokenFilePath = this.credsFilePath || path.join(this.credPath, KIRO_AUTH_TOKEN_FILE);
|
|
||||||
const updatedTokenData = {
|
const updatedTokenData = {
|
||||||
accessToken: this.accessToken,
|
accessToken: this.accessToken,
|
||||||
refreshToken: this.refreshToken,
|
refreshToken: this.refreshToken,
|
||||||
expiresAt: expiresAt,
|
expiresAt: expiresAt,
|
||||||
};
|
};
|
||||||
if(this.profileArn){
|
if (this.profileArn) {
|
||||||
updatedTokenData.profileArn = this.profileArn;
|
updatedTokenData.profileArn = this.profileArn;
|
||||||
}
|
}
|
||||||
await saveCredentialsToFile(tokenFilePath, updatedTokenData);
|
await saveCredentialsToFile(tokenFilePath, updatedTokenData);
|
||||||
|
|
@ -580,10 +647,39 @@ async initializeAuth(forceRefresh = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.accessToken) {
|
/**
|
||||||
throw new Error('No access token available after initialization and refresh attempts.');
|
* 在并发刷新完成后重新加载凭证(内部方法)
|
||||||
|
* @param {string} tokenFilePath - 凭证文件路径
|
||||||
|
*/
|
||||||
|
async _reloadCredentialsAfterRefresh(tokenFilePath) {
|
||||||
|
try {
|
||||||
|
const fileContent = await fs.readFile(tokenFilePath, 'utf8');
|
||||||
|
let credentials;
|
||||||
|
try {
|
||||||
|
credentials = JSON.parse(fileContent);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('[Kiro Auth] JSON parse failed, attempting repair...');
|
||||||
|
try {
|
||||||
|
const repaired = repairJson(fileContent);
|
||||||
|
credentials = JSON.parse(repaired);
|
||||||
|
console.info('[Kiro Auth] JSON repair successful');
|
||||||
|
} catch (repairError) {
|
||||||
|
console.error('[Kiro Auth] JSON repair failed:', repairError.message);
|
||||||
|
throw new Error(`Failed to parse credentials file after repair attempt: ${repairError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.accessToken = credentials.accessToken;
|
||||||
|
this.refreshToken = credentials.refreshToken;
|
||||||
|
this.expiresAt = credentials.expiresAt;
|
||||||
|
if (credentials.profileArn) {
|
||||||
|
this.profileArn = credentials.profileArn;
|
||||||
|
}
|
||||||
|
console.debug('[Kiro Auth] Credentials reloaded after concurrent refresh');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[Kiro Auth] Failed to reload credentials after refresh: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract text content from OpenAI message format
|
* Extract text content from OpenAI message format
|
||||||
|
|
@ -1198,7 +1294,21 @@ async initializeAuth(forceRefresh = false) {
|
||||||
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
||||||
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
|
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
|
||||||
|
|
||||||
const requestData = this.buildCodewhispererRequest(body.messages, model, body.tools, body.system, body.thinking);
|
// 处理不同格式的请求体(messages 或 contents)
|
||||||
|
let messages = body.messages;
|
||||||
|
if (!messages && body.contents) {
|
||||||
|
// 将 Gemini 格式的 contents 转换为 messages 格式
|
||||||
|
messages = body.contents.map(content => ({
|
||||||
|
role: content.role || 'user',
|
||||||
|
content: content.parts?.map(part => part.text).join('') || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
||||||
|
throw new Error('No messages found in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestData = this.buildCodewhispererRequest(messages, model, body.tools, body.system, body.thinking);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = this.accessToken; // Use the already initialized token
|
const token = this.accessToken; // Use the already initialized token
|
||||||
|
|
@ -1219,16 +1329,25 @@ async initializeAuth(forceRefresh = false) {
|
||||||
// 检查是否为可重试的网络错误
|
// 检查是否为可重试的网络错误
|
||||||
const isNetworkError = isRetryableNetworkError(error);
|
const isNetworkError = isRetryableNetworkError(error);
|
||||||
|
|
||||||
// Handle 401 (Unauthorized) - try to refresh token first
|
// Handle 401 (Unauthorized) - refresh UUID first, then try to refresh token
|
||||||
if (status === 401 && !isRetry) {
|
if (status === 401 && !isRetry) {
|
||||||
console.log('[Kiro] Received 401. Attempting token refresh...');
|
console.log('[Kiro] Received 401. Refreshing UUID and attempting token refresh...');
|
||||||
|
|
||||||
|
// 1. 先刷新 UUID
|
||||||
|
const newUuid = this._refreshUuid();
|
||||||
|
if (newUuid) {
|
||||||
|
console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`);
|
||||||
|
this.uuid = newUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试刷新 token
|
||||||
try {
|
try {
|
||||||
await this.initializeAuth(true); // Force refresh token
|
await this.initializeAuth(true); // Force refresh token
|
||||||
console.log('[Kiro] Token refresh successful after 401, retrying request...');
|
console.log('[Kiro] Token refresh successful after 401, retrying request with new UUID...');
|
||||||
return this.callApi(method, model, body, true, retryCount);
|
return this.callApi(method, model, body, true, retryCount);
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
console.error('[Kiro] Token refresh failed during 401 retry:', refreshError.message);
|
console.error('[Kiro] Token refresh failed during 401 retry:', refreshError.message);
|
||||||
// Mark credential as unhealthy immediately and attach marker to error
|
// 3. 刷新失败,标记凭证不健康,让上层切换到其他凭证
|
||||||
this._markCredentialUnhealthy('401 Unauthorized - Token refresh failed', refreshError);
|
this._markCredentialUnhealthy('401 Unauthorized - Token refresh failed', refreshError);
|
||||||
throw refreshError;
|
throw refreshError;
|
||||||
}
|
}
|
||||||
|
|
@ -1271,6 +1390,25 @@ async initializeAuth(forceRefresh = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to refresh the current credential's UUID
|
||||||
|
* Used when encountering 401 errors to get a fresh identity
|
||||||
|
* @returns {string|null} - The new UUID, or null if refresh failed
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_refreshUuid() {
|
||||||
|
const poolManager = getProviderPoolManager();
|
||||||
|
if (poolManager && this.uuid) {
|
||||||
|
const newUuid = poolManager.refreshProviderUuid(MODEL_PROVIDER.KIRO_API, {
|
||||||
|
uuid: this.uuid
|
||||||
|
});
|
||||||
|
return newUuid;
|
||||||
|
} else {
|
||||||
|
console.warn(`[Kiro] Cannot refresh UUID: poolManager=${!!poolManager}, uuid=${this.uuid}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to mark the current credential as unhealthy
|
* Helper method to mark the current credential as unhealthy
|
||||||
* @param {string} reason - The reason for marking unhealthy
|
* @param {string} reason - The reason for marking unhealthy
|
||||||
|
|
@ -1518,7 +1656,21 @@ async initializeAuth(forceRefresh = false) {
|
||||||
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
||||||
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000;
|
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000;
|
||||||
|
|
||||||
const requestData = this.buildCodewhispererRequest(body.messages, model, body.tools, body.system, body.thinking);
|
// 处理不同格式的请求体(messages 或 contents)
|
||||||
|
let messages = body.messages;
|
||||||
|
if (!messages && body.contents) {
|
||||||
|
// 将 Gemini 格式的 contents 转换为 messages 格式
|
||||||
|
messages = body.contents.map(content => ({
|
||||||
|
role: content.role || 'user',
|
||||||
|
content: content.parts?.map(part => part.text).join('') || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
||||||
|
throw new Error('No messages found in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestData = this.buildCodewhispererRequest(messages, model, body.tools, body.system, body.thinking);
|
||||||
|
|
||||||
const token = this.accessToken;
|
const token = this.accessToken;
|
||||||
const headers = {
|
const headers = {
|
||||||
|
|
|
||||||
|
|
@ -556,6 +556,52 @@ export class ProviderPoolManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新指定提供商的 UUID
|
||||||
|
* 用于在认证错误(如 401)时更换 UUID,以便重新尝试
|
||||||
|
* @param {string} providerType - 提供商类型
|
||||||
|
* @param {object} providerConfig - 提供商配置(包含当前 uuid)
|
||||||
|
* @returns {string|null} 新的 UUID,如果失败则返回 null
|
||||||
|
*/
|
||||||
|
refreshProviderUuid(providerType, providerConfig) {
|
||||||
|
if (!providerConfig?.uuid) {
|
||||||
|
this._log('error', 'Invalid providerConfig in refreshProviderUuid');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = this._findProvider(providerType, providerConfig.uuid);
|
||||||
|
if (provider) {
|
||||||
|
const oldUuid = provider.config.uuid;
|
||||||
|
// 生成新的 UUID
|
||||||
|
const newUuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新 provider 的 UUID
|
||||||
|
provider.uuid = newUuid;
|
||||||
|
provider.config.uuid = newUuid;
|
||||||
|
|
||||||
|
// 同时更新 providerPools 中的原始数据
|
||||||
|
const poolArray = this.providerPools[providerType];
|
||||||
|
if (poolArray) {
|
||||||
|
const originalProvider = poolArray.find(p => p.uuid === oldUuid);
|
||||||
|
if (originalProvider) {
|
||||||
|
originalProvider.uuid = newUuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._log('info', `Refreshed provider UUID: ${oldUuid} -> ${newUuid} for type ${providerType}`);
|
||||||
|
this._debouncedSave(providerType);
|
||||||
|
|
||||||
|
return newUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._log('warn', `Provider not found for UUID refresh: ${providerConfig.uuid} in ${providerType}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs health checks on all providers in the pool.
|
* Performs health checks on all providers in the pool.
|
||||||
* This method would typically be called periodically (e.g., via cron job).
|
* This method would typically be called periodically (e.g., via cron job).
|
||||||
|
|
@ -638,22 +684,13 @@ export class ProviderPoolManager {
|
||||||
return requests;
|
return requests;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kiro OAuth 同时支持 messages 和 contents 格式
|
// Kiro OAuth 只支持 messages 格式
|
||||||
if (providerType.startsWith('claude-kiro')) {
|
if (providerType.startsWith('claude-kiro')) {
|
||||||
// 优先使用 messages 格式
|
|
||||||
requests.push({
|
requests.push({
|
||||||
messages: [baseMessage],
|
messages: [baseMessage],
|
||||||
model: modelName,
|
model: modelName,
|
||||||
max_tokens: 1
|
max_tokens: 1
|
||||||
});
|
});
|
||||||
// 备用 contents 格式
|
|
||||||
requests.push({
|
|
||||||
contents: [{
|
|
||||||
role: 'user',
|
|
||||||
parts: [{ text: baseMessage.content }]
|
|
||||||
}],
|
|
||||||
max_tokens: 1
|
|
||||||
});
|
|
||||||
return requests;
|
return requests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -275,6 +275,8 @@ const translations = {
|
||||||
'config.advanced.promptLogMode.file': '文件 (file)',
|
'config.advanced.promptLogMode.file': '文件 (file)',
|
||||||
'config.advanced.maxRetries': '最大重试次数',
|
'config.advanced.maxRetries': '最大重试次数',
|
||||||
'config.advanced.baseDelay': '重试基础延迟(毫秒)',
|
'config.advanced.baseDelay': '重试基础延迟(毫秒)',
|
||||||
|
'config.advanced.credentialSwitchMaxRetries': '坏凭证切换最大重试次数',
|
||||||
|
'config.advanced.credentialSwitchMaxRetriesNote': '认证错误(401/403)后切换凭证的最大重试次数,默认 5 次',
|
||||||
'config.advanced.cronInterval': 'OAuth令牌刷新间隔(分钟)',
|
'config.advanced.cronInterval': 'OAuth令牌刷新间隔(分钟)',
|
||||||
'config.advanced.cronEnabled': '启用OAuth令牌自动刷新(需重启服务)',
|
'config.advanced.cronEnabled': '启用OAuth令牌自动刷新(需重启服务)',
|
||||||
'config.advanced.poolFilePath': '提供商池配置文件路径(不能为空)',
|
'config.advanced.poolFilePath': '提供商池配置文件路径(不能为空)',
|
||||||
|
|
@ -835,6 +837,8 @@ const translations = {
|
||||||
'config.advanced.promptLogMode.file': 'File',
|
'config.advanced.promptLogMode.file': 'File',
|
||||||
'config.advanced.maxRetries': 'Max Retries',
|
'config.advanced.maxRetries': 'Max Retries',
|
||||||
'config.advanced.baseDelay': 'Base Retry Delay (ms)',
|
'config.advanced.baseDelay': 'Base Retry Delay (ms)',
|
||||||
|
'config.advanced.credentialSwitchMaxRetries': 'Credential Switch Max Retries',
|
||||||
|
'config.advanced.credentialSwitchMaxRetriesNote': 'Max retry count for switching credentials after auth errors (401/403), default 5',
|
||||||
'config.advanced.cronInterval': 'OAuth Token Refresh Interval (minutes)',
|
'config.advanced.cronInterval': 'OAuth Token Refresh Interval (minutes)',
|
||||||
'config.advanced.cronEnabled': 'Enable OAuth Token Auto Refresh (requires restart)',
|
'config.advanced.cronEnabled': 'Enable OAuth Token Auto Refresh (requires restart)',
|
||||||
'config.advanced.poolFilePath': 'Provider Pool Config File Path (required)',
|
'config.advanced.poolFilePath': 'Provider Pool Config File Path (required)',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue