feat(provider): 添加健康状态跟踪并持久化到JSON文件
添加provider健康状态跟踪功能,包括使用次数、错误计数和最后使用时间 将provider状态持久化到JSON文件,确保重启后状态不丢失 重构provider选择逻辑,将状态管理移至config对象
This commit is contained in:
parent
3109eb21c5
commit
b60593085e
2 changed files with 125 additions and 32 deletions
|
|
@ -3,46 +3,86 @@
|
|||
{
|
||||
"OPENAI_API_KEY": "sk-openai-key1",
|
||||
"OPENAI_BASE_URL": "https://api.openai.com/v1",
|
||||
"uuid": "2f579c65-d3c5-41b1-9985-9f6e3d7bf39c"
|
||||
"uuid": "2f579c65-d3c5-41b1-9985-9f6e3d7bf39c",
|
||||
"isHealthy": true,
|
||||
"lastUsed": null,
|
||||
"usageCount": 0,
|
||||
"errorCount": 0,
|
||||
"lastErrorTime": null
|
||||
},
|
||||
{
|
||||
"OPENAI_API_KEY": "sk-openai-key2",
|
||||
"OPENAI_BASE_URL": "https://api.openai.com/v1",
|
||||
"uuid": "e284628d-302f-456d-91f3-6095386fb3b8"
|
||||
"uuid": "e284628d-302f-456d-91f3-6095386fb3b8",
|
||||
"isHealthy": true,
|
||||
"lastUsed": null,
|
||||
"usageCount": 0,
|
||||
"errorCount": 0,
|
||||
"lastErrorTime": null
|
||||
}
|
||||
],
|
||||
"gemini-cli": [
|
||||
"gemini-cli-oauth": [
|
||||
{
|
||||
"GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials1.json",
|
||||
"PROJECT_ID": "your-project-id-1",
|
||||
"uuid": "ac200154-26b8-4f5f-8650-e8cc738b06e3"
|
||||
"uuid": "ac200154-26b8-4f5f-8650-e8cc738b06e3",
|
||||
"isHealthy": true,
|
||||
"lastUsed": null,
|
||||
"usageCount": 0,
|
||||
"errorCount": 0,
|
||||
"lastErrorTime": null
|
||||
},
|
||||
{
|
||||
"GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials2.json",
|
||||
"PROJECT_ID": "your-project-id-2",
|
||||
"uuid": "4f8afcc2-a9bb-4b96-bb50-3b9667a71f54"
|
||||
"uuid": "4f8afcc2-a9bb-4b96-bb50-3b9667a71f54",
|
||||
"isHealthy": true,
|
||||
"lastUsed": null,
|
||||
"usageCount": 0,
|
||||
"errorCount": 0,
|
||||
"lastErrorTime": null
|
||||
}
|
||||
],
|
||||
"claude-custom": [
|
||||
{
|
||||
"CLAUDE_API_KEY": "sk-claude-key1",
|
||||
"CLAUDE_BASE_URL": "https://api.anthropic.com",
|
||||
"uuid": "bb87047a-3b1d-4249-adbb-1087ecd58128"
|
||||
"uuid": "bb87047a-3b1d-4249-adbb-1087ecd58128",
|
||||
"isHealthy": true,
|
||||
"lastUsed": null,
|
||||
"usageCount": 0,
|
||||
"errorCount": 0,
|
||||
"lastErrorTime": null
|
||||
},
|
||||
{
|
||||
"CLAUDE_API_KEY": "sk-claude-key2",
|
||||
"CLAUDE_BASE_URL": "https://api.anthropic.com",
|
||||
"uuid": "7c2002c6-122a-4db0-af06-8a0ff433801a"
|
||||
"uuid": "7c2002c6-122a-4db0-af06-8a0ff433801a",
|
||||
"isHealthy": true,
|
||||
"lastUsed": null,
|
||||
"usageCount": 0,
|
||||
"errorCount": 0,
|
||||
"lastErrorTime": null
|
||||
}
|
||||
],
|
||||
"kiro-api": [
|
||||
"claude-kiro-oauth": [
|
||||
{
|
||||
"KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds1.json",
|
||||
"uuid": "2c69d0ac-b86f-43d8-9d17-0d300afc5cfd"
|
||||
"uuid": "2c69d0ac-b86f-43d8-9d17-0d300afc5cfd",
|
||||
"isHealthy": true,
|
||||
"lastUsed": null,
|
||||
"usageCount": 0,
|
||||
"errorCount": 0,
|
||||
"lastErrorTime": null
|
||||
},
|
||||
{
|
||||
"KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds2.json",
|
||||
"uuid": "7482abe6-8083-4288-bb7d-d8ecb7c461e2"
|
||||
"uuid": "7482abe6-8083-4288-bb7d-d8ecb7c461e2",
|
||||
"isHealthy": true,
|
||||
"lastUsed": null,
|
||||
"usageCount": 0,
|
||||
"errorCount": 0,
|
||||
"lastErrorTime": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import * as fs from 'fs'; // Import fs module
|
||||
|
||||
/**
|
||||
* Manages a pool of API service providers, handling their health and selection.
|
||||
*/
|
||||
|
|
@ -19,15 +21,21 @@ export class ProviderPoolManager {
|
|||
for (const providerType in this.providerPools) {
|
||||
this.providerStatus[providerType] = [];
|
||||
this.roundRobinIndex[providerType] = 0; // Initialize round-robin index for each type
|
||||
this.providerPools[providerType].forEach((providerConfig, index) => {
|
||||
this.providerPools[providerType].forEach((providerConfig) => {
|
||||
// Ensure initial health and usage stats are present in the config
|
||||
providerConfig.isHealthy = providerConfig.isHealthy !== undefined ? providerConfig.isHealthy : true;
|
||||
providerConfig.lastUsed = providerConfig.lastUsed !== undefined ? providerConfig.lastUsed : null;
|
||||
providerConfig.usageCount = providerConfig.usageCount !== undefined ? providerConfig.usageCount : 0;
|
||||
providerConfig.errorCount = providerConfig.errorCount !== undefined ? providerConfig.errorCount : 0;
|
||||
if (providerConfig.lastErrorTime && typeof providerConfig.lastErrorTime === 'string') {
|
||||
providerConfig.lastErrorTime = new Date(providerConfig.lastErrorTime);
|
||||
} else if (providerConfig.lastErrorTime === undefined) {
|
||||
providerConfig.lastErrorTime = null;
|
||||
}
|
||||
|
||||
this.providerStatus[providerType].push({
|
||||
config: providerConfig,
|
||||
uuid: providerConfig.uuid,
|
||||
isHealthy: true,
|
||||
lastUsed: null,
|
||||
usageCount: 0,
|
||||
errorCount: 0,
|
||||
lastErrorTime: null, // New: Timestamp of the last error
|
||||
uuid: providerConfig.uuid, // Still keep uuid at the top level for easy access
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -42,7 +50,7 @@ export class ProviderPoolManager {
|
|||
*/
|
||||
selectProvider(providerType) {
|
||||
const availableProviders = this.providerStatus[providerType] || [];
|
||||
const healthyProviders = availableProviders.filter(p => p.isHealthy);
|
||||
const healthyProviders = availableProviders.filter(p => p.config.isHealthy);
|
||||
|
||||
if (healthyProviders.length === 0) {
|
||||
console.warn(`[ProviderPoolManager] No healthy providers available for type: ${providerType}`);
|
||||
|
|
@ -65,9 +73,11 @@ export class ProviderPoolManager {
|
|||
}
|
||||
|
||||
if (selected) {
|
||||
selected.lastUsed = new Date();
|
||||
selected.usageCount++; // Increment usage count
|
||||
selected.config.lastUsed = new Date();
|
||||
selected.config.usageCount++; // Increment usage count
|
||||
|
||||
console.log(`[ProviderPoolManager] Selected provider for ${providerType} (round-robin): ${JSON.stringify(selected.config)}`);
|
||||
this._saveProviderPoolsToJson(providerType); // Persist changes
|
||||
return selected.config;
|
||||
}
|
||||
|
||||
|
|
@ -84,15 +94,16 @@ export class ProviderPoolManager {
|
|||
if (pool) {
|
||||
const provider = pool.find(p => p.uuid === providerConfig.uuid);
|
||||
if (provider) {
|
||||
provider.errorCount++;
|
||||
provider.lastErrorTime = new Date(); // Update last error time
|
||||
provider.config.errorCount++;
|
||||
provider.config.lastErrorTime = new Date(); // Update last error time in config
|
||||
|
||||
if (provider.errorCount >= this.maxErrorCount) {
|
||||
provider.isHealthy = false;
|
||||
console.warn(`[ProviderPoolManager] Marked provider as unhealthy: ${JSON.stringify(providerConfig)} for type ${providerType}. Total errors: ${provider.errorCount}`);
|
||||
if (provider.config.errorCount >= this.maxErrorCount) {
|
||||
provider.config.isHealthy = false;
|
||||
console.warn(`[ProviderPoolManager] Marked provider as unhealthy: ${JSON.stringify(providerConfig)} for type ${providerType}. Total errors: ${provider.config.errorCount}`);
|
||||
} else {
|
||||
console.warn(`[ProviderPoolManager] Provider ${JSON.stringify(providerConfig)} for type ${providerType} error count: ${provider.errorCount}/${this.maxErrorCount}. Still healthy.`);
|
||||
console.warn(`[ProviderPoolManager] Provider ${JSON.stringify(providerConfig)} for type ${providerType} error count: ${provider.config.errorCount}/${this.maxErrorCount}. Still healthy.`);
|
||||
}
|
||||
this._saveProviderPoolsToJson(providerType); // Persist changes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -105,11 +116,13 @@ export class ProviderPoolManager {
|
|||
markProviderHealthy(providerType, providerConfig) {
|
||||
const pool = this.providerStatus[providerType];
|
||||
if (pool) {
|
||||
const provider = pool.find(p => p.uuid === providerConfig.uuid);a
|
||||
const provider = pool.find(p => p.uuid === providerConfig.uuid);
|
||||
if (provider) {
|
||||
provider.isHealthy = true;
|
||||
provider.errorCount = 0; // Reset error count on health recovery
|
||||
provider.config.isHealthy = true;
|
||||
provider.config.errorCount = 0; // Reset error count on health recovery
|
||||
provider.config.lastErrorTime = null; // Reset lastErrorTime when healthy
|
||||
console.log(`[ProviderPoolManager] Marked provider as healthy: ${JSON.stringify(providerConfig)} for type ${providerType}`);
|
||||
this._saveProviderPoolsToJson(providerType); // Persist changes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -126,8 +139,8 @@ export class ProviderPoolManager {
|
|||
const providerConfig = providerStatus.config;
|
||||
|
||||
// Only attempt to health check unhealthy providers after a certain interval
|
||||
if (!providerStatus.isHealthy && providerStatus.lastErrorTime &&
|
||||
(now.getTime() - providerStatus.lastErrorTime.getTime() < this.healthCheckInterval)) {
|
||||
if (!providerStatus.config.isHealthy && providerStatus.config.lastErrorTime &&
|
||||
(now.getTime() - providerStatus.config.lastErrorTime.getTime() < this.healthCheckInterval)) {
|
||||
console.log(`[ProviderPoolManager] Skipping health check for ${JSON.stringify(providerConfig)} (${providerType}). Last error too recent.`);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -137,7 +150,7 @@ export class ProviderPoolManager {
|
|||
// For now, if a provider was unhealthy and enough time has passed,
|
||||
// we optimistically mark it healthy and reset error count.
|
||||
// A more robust system would involve actual API calls or pings here.
|
||||
if (!providerStatus.isHealthy) {
|
||||
if (!providerStatus.config.isHealthy) {
|
||||
// Only reset and mark healthy if it was unhealthy and we are attempting a check after interval
|
||||
this.markProviderHealthy(providerType, providerConfig);
|
||||
console.log(`[ProviderPoolManager] Health check for ${JSON.stringify(providerConfig)} (${providerType}): Marked Healthy (re-evaluation)`);
|
||||
|
|
@ -154,4 +167,44 @@ export class ProviderPoolManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Saves the current provider pools configuration to the JSON file.
|
||||
* @private
|
||||
*/
|
||||
async _saveProviderPoolsToJson(providerTypeToUpdate) {
|
||||
try {
|
||||
const filePath = 'provider_pools.json';
|
||||
let currentPools = {};
|
||||
try {
|
||||
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
||||
currentPools = JSON.parse(fileContent);
|
||||
} catch (readError) {
|
||||
if (readError.code === 'ENOENT') {
|
||||
console.log('[ProviderPoolManager] provider_pools.json does not exist, creating new file.');
|
||||
} else {
|
||||
throw readError;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.providerStatus[providerTypeToUpdate]) {
|
||||
currentPools[providerTypeToUpdate] = this.providerStatus[providerTypeToUpdate].map(p => {
|
||||
if (p.config.lastUsed instanceof Date) {
|
||||
p.config.lastUsed = p.config.lastUsed.toISOString();
|
||||
}
|
||||
if (p.config.lastErrorTime instanceof Date) {
|
||||
p.config.lastErrorTime = p.config.lastErrorTime.toISOString();
|
||||
}
|
||||
return p.config;
|
||||
});
|
||||
} else {
|
||||
console.warn(`[ProviderPoolManager] Attempted to save unknown providerType: ${providerTypeToUpdate}`);
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(filePath, JSON.stringify(currentPools, null, 2), 'utf8');
|
||||
console.log(`[ProviderPoolManager] provider_pools.json for ${providerTypeToUpdate} updated successfully.`);
|
||||
} catch (error) {
|
||||
console.error('[ProviderPoolManager] Failed to write provider_pools.json:', error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue