Service console check providers
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import type { ProviderConfig, StatusCheckResult, ApiResponse } from './types';
|
||||
|
||||
export abstract class BaseProviderChecker {
|
||||
protected config: ProviderConfig;
|
||||
|
||||
constructor(config: ProviderConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
protected async checkApiEndpoint(
|
||||
url: string,
|
||||
headers?: Record<string, string>,
|
||||
testModel?: string,
|
||||
): Promise<{ ok: boolean; status: number | string; message?: string; responseTime: number }> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Add common headers
|
||||
const processedHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: processedHeaders,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const endTime = performance.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const data = (await response.json()) as ApiResponse;
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `API returned status: ${response.status}`;
|
||||
|
||||
if (data.error?.message) {
|
||||
errorMessage = data.error.message;
|
||||
} else if (data.message) {
|
||||
errorMessage = data.message;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
message: errorMessage,
|
||||
responseTime,
|
||||
};
|
||||
}
|
||||
|
||||
// Different providers have different model list formats
|
||||
let models: string[] = [];
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
models = data.map((model: { id?: string; name?: string }) => model.id || model.name || '');
|
||||
} else if (data.data && Array.isArray(data.data)) {
|
||||
models = data.data.map((model) => model.id || model.name || '');
|
||||
} else if (data.models && Array.isArray(data.models)) {
|
||||
models = data.models.map((model) => model.id || model.name || '');
|
||||
} else if (data.model) {
|
||||
models = [data.model];
|
||||
}
|
||||
|
||||
if (!testModel || models.length > 0) {
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
responseTime,
|
||||
message: 'API key is valid',
|
||||
};
|
||||
}
|
||||
|
||||
if (testModel && !models.includes(testModel)) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 'model_not_found',
|
||||
message: `API key is valid (test model ${testModel} not found in ${models.length} available models)`,
|
||||
responseTime,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
message: 'API key is valid',
|
||||
responseTime,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error checking API endpoint ${url}:`, error);
|
||||
return {
|
||||
ok: false,
|
||||
status: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: error instanceof Error ? `Connection failed: ${error.message}` : 'Connection failed',
|
||||
responseTime: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async checkEndpoint(url: string): Promise<'reachable' | 'unreachable'> {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
mode: 'no-cors',
|
||||
headers: {
|
||||
Accept: 'text/html',
|
||||
},
|
||||
});
|
||||
return response.type === 'opaque' ? 'reachable' : 'unreachable';
|
||||
} catch (error) {
|
||||
console.error(`Error checking ${url}:`, error);
|
||||
return 'unreachable';
|
||||
}
|
||||
}
|
||||
|
||||
abstract checkStatus(): Promise<StatusCheckResult>;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { ProviderName, ProviderConfig, StatusCheckResult } from './types';
|
||||
import { OpenAIStatusChecker } from './providers/openai';
|
||||
import { BaseProviderChecker } from './base-provider';
|
||||
|
||||
// Import other provider implementations as they are created
|
||||
|
||||
export class ProviderStatusCheckerFactory {
|
||||
private static _providerConfigs: Record<ProviderName, ProviderConfig> = {
|
||||
OpenAI: {
|
||||
statusUrl: 'https://status.openai.com/',
|
||||
apiUrl: 'https://api.openai.com/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $OPENAI_API_KEY',
|
||||
},
|
||||
testModel: 'gpt-3.5-turbo',
|
||||
},
|
||||
Anthropic: {
|
||||
statusUrl: 'https://status.anthropic.com/',
|
||||
apiUrl: 'https://api.anthropic.com/v1/messages',
|
||||
headers: {
|
||||
'x-api-key': '$ANTHROPIC_API_KEY',
|
||||
'anthropic-version': '2024-02-29',
|
||||
},
|
||||
testModel: 'claude-3-sonnet-20240229',
|
||||
},
|
||||
AmazonBedrock: {
|
||||
statusUrl: 'https://health.aws.amazon.com/health/status',
|
||||
apiUrl: 'https://bedrock.us-east-1.amazonaws.com/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $AWS_BEDROCK_CONFIG',
|
||||
},
|
||||
testModel: 'anthropic.claude-3-sonnet-20240229-v1:0',
|
||||
},
|
||||
Cohere: {
|
||||
statusUrl: 'https://status.cohere.com/',
|
||||
apiUrl: 'https://api.cohere.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $COHERE_API_KEY',
|
||||
},
|
||||
testModel: 'command',
|
||||
},
|
||||
Deepseek: {
|
||||
statusUrl: 'https://status.deepseek.com/',
|
||||
apiUrl: 'https://api.deepseek.com/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $DEEPSEEK_API_KEY',
|
||||
},
|
||||
testModel: 'deepseek-chat',
|
||||
},
|
||||
Google: {
|
||||
statusUrl: 'https://status.cloud.google.com/',
|
||||
apiUrl: 'https://generativelanguage.googleapis.com/v1/models',
|
||||
headers: {
|
||||
'x-goog-api-key': '$GOOGLE_API_KEY',
|
||||
},
|
||||
testModel: 'gemini-pro',
|
||||
},
|
||||
Groq: {
|
||||
statusUrl: 'https://groqstatus.com/',
|
||||
apiUrl: 'https://api.groq.com/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $GROQ_API_KEY',
|
||||
},
|
||||
testModel: 'mixtral-8x7b-32768',
|
||||
},
|
||||
HuggingFace: {
|
||||
statusUrl: 'https://status.huggingface.co/',
|
||||
apiUrl: 'https://api-inference.huggingface.co/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $HUGGINGFACE_API_KEY',
|
||||
},
|
||||
testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
|
||||
},
|
||||
Hyperbolic: {
|
||||
statusUrl: 'https://status.hyperbolic.ai/',
|
||||
apiUrl: 'https://api.hyperbolic.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $HYPERBOLIC_API_KEY',
|
||||
},
|
||||
testModel: 'hyperbolic-1',
|
||||
},
|
||||
Mistral: {
|
||||
statusUrl: 'https://status.mistral.ai/',
|
||||
apiUrl: 'https://api.mistral.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $MISTRAL_API_KEY',
|
||||
},
|
||||
testModel: 'mistral-tiny',
|
||||
},
|
||||
OpenRouter: {
|
||||
statusUrl: 'https://status.openrouter.ai/',
|
||||
apiUrl: 'https://openrouter.ai/api/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $OPEN_ROUTER_API_KEY',
|
||||
},
|
||||
testModel: 'anthropic/claude-3-sonnet',
|
||||
},
|
||||
Perplexity: {
|
||||
statusUrl: 'https://status.perplexity.com/',
|
||||
apiUrl: 'https://api.perplexity.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $PERPLEXITY_API_KEY',
|
||||
},
|
||||
testModel: 'pplx-7b-chat',
|
||||
},
|
||||
Together: {
|
||||
statusUrl: 'https://status.together.ai/',
|
||||
apiUrl: 'https://api.together.xyz/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $TOGETHER_API_KEY',
|
||||
},
|
||||
testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
|
||||
},
|
||||
XAI: {
|
||||
statusUrl: 'https://status.x.ai/',
|
||||
apiUrl: 'https://api.x.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $XAI_API_KEY',
|
||||
},
|
||||
testModel: 'grok-1',
|
||||
},
|
||||
};
|
||||
|
||||
static getChecker(provider: ProviderName): BaseProviderChecker {
|
||||
const config = this._providerConfigs[provider];
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`No configuration found for provider: ${provider}`);
|
||||
}
|
||||
|
||||
// Return specific provider implementation or fallback to base implementation
|
||||
switch (provider) {
|
||||
case 'OpenAI':
|
||||
return new OpenAIStatusChecker(config);
|
||||
|
||||
// Add other provider implementations as they are created
|
||||
default:
|
||||
return new (class extends BaseProviderChecker {
|
||||
async checkStatus(): Promise<StatusCheckResult> {
|
||||
const endpointStatus = await this.checkEndpoint(this.config.statusUrl);
|
||||
const apiStatus = await this.checkEndpoint(this.config.apiUrl);
|
||||
|
||||
return {
|
||||
status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
|
||||
message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
|
||||
incidents: ['Note: Limited status information due to CORS restrictions'],
|
||||
};
|
||||
}
|
||||
})(config);
|
||||
}
|
||||
}
|
||||
|
||||
static getProviderNames(): ProviderName[] {
|
||||
return Object.keys(this._providerConfigs) as ProviderName[];
|
||||
}
|
||||
|
||||
static getProviderConfig(provider: ProviderName): ProviderConfig | undefined {
|
||||
return this._providerConfigs[provider];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
|
||||
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
|
||||
|
||||
export class OpenAIStatusChecker extends BaseProviderChecker {
|
||||
async checkStatus(): Promise<StatusCheckResult> {
|
||||
try {
|
||||
// Check status page
|
||||
const statusPageResponse = await fetch('https://status.openai.com/');
|
||||
const text = await statusPageResponse.text();
|
||||
|
||||
// Check individual services
|
||||
const services = {
|
||||
api: {
|
||||
operational: text.includes('API ? Operational'),
|
||||
degraded: text.includes('API ? Degraded Performance'),
|
||||
outage: text.includes('API ? Major Outage') || text.includes('API ? Partial Outage'),
|
||||
},
|
||||
chat: {
|
||||
operational: text.includes('ChatGPT ? Operational'),
|
||||
degraded: text.includes('ChatGPT ? Degraded Performance'),
|
||||
outage: text.includes('ChatGPT ? Major Outage') || text.includes('ChatGPT ? Partial Outage'),
|
||||
},
|
||||
};
|
||||
|
||||
// Extract recent incidents
|
||||
const incidents: string[] = [];
|
||||
const incidentMatches = text.match(/Past Incidents(.*?)(?=\w+ \d+, \d{4})/s);
|
||||
|
||||
if (incidentMatches) {
|
||||
const recentIncidents = incidentMatches[1]
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && line.includes('202')); // Get only dated incidents
|
||||
|
||||
incidents.push(...recentIncidents.slice(0, 5));
|
||||
}
|
||||
|
||||
// Determine overall status
|
||||
let status: StatusCheckResult['status'] = 'operational';
|
||||
const messages: string[] = [];
|
||||
|
||||
if (services.api.outage || services.chat.outage) {
|
||||
status = 'down';
|
||||
|
||||
if (services.api.outage) {
|
||||
messages.push('API: Major Outage');
|
||||
}
|
||||
|
||||
if (services.chat.outage) {
|
||||
messages.push('ChatGPT: Major Outage');
|
||||
}
|
||||
} else if (services.api.degraded || services.chat.degraded) {
|
||||
status = 'degraded';
|
||||
|
||||
if (services.api.degraded) {
|
||||
messages.push('API: Degraded Performance');
|
||||
}
|
||||
|
||||
if (services.chat.degraded) {
|
||||
messages.push('ChatGPT: Degraded Performance');
|
||||
}
|
||||
} else if (services.api.operational) {
|
||||
messages.push('API: Operational');
|
||||
}
|
||||
|
||||
// If status page check fails, fallback to endpoint check
|
||||
if (!statusPageResponse.ok) {
|
||||
const endpointStatus = await this.checkEndpoint('https://status.openai.com/');
|
||||
const apiEndpoint = 'https://api.openai.com/v1/models';
|
||||
const apiStatus = await this.checkEndpoint(apiEndpoint);
|
||||
|
||||
return {
|
||||
status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
|
||||
message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
|
||||
incidents: ['Note: Limited status information due to CORS restrictions'],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
message: messages.join(', ') || 'Status unknown',
|
||||
incidents,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking OpenAI status:', error);
|
||||
|
||||
// Fallback to basic endpoint check
|
||||
const endpointStatus = await this.checkEndpoint('https://status.openai.com/');
|
||||
const apiEndpoint = 'https://api.openai.com/v1/models';
|
||||
const apiStatus = await this.checkEndpoint(apiEndpoint);
|
||||
|
||||
return {
|
||||
status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
|
||||
message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
|
||||
incidents: ['Note: Limited status information due to CORS restrictions'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
58
app/components/settings/providers/service-status/types.ts
Normal file
58
app/components/settings/providers/service-status/types.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { IconType } from 'react-icons';
|
||||
|
||||
export type ProviderName =
|
||||
| 'AmazonBedrock'
|
||||
| 'Anthropic'
|
||||
| 'Cohere'
|
||||
| 'Deepseek'
|
||||
| 'Google'
|
||||
| 'Groq'
|
||||
| 'HuggingFace'
|
||||
| 'Hyperbolic'
|
||||
| 'Mistral'
|
||||
| 'OpenAI'
|
||||
| 'OpenRouter'
|
||||
| 'Perplexity'
|
||||
| 'Together'
|
||||
| 'XAI';
|
||||
|
||||
export type ServiceStatus = {
|
||||
provider: ProviderName;
|
||||
status: 'operational' | 'degraded' | 'down';
|
||||
lastChecked: string;
|
||||
statusUrl?: string;
|
||||
icon?: IconType;
|
||||
message?: string;
|
||||
responseTime?: number;
|
||||
incidents?: string[];
|
||||
};
|
||||
|
||||
export type ProviderConfig = {
|
||||
statusUrl: string;
|
||||
apiUrl: string;
|
||||
headers: Record<string, string>;
|
||||
testModel: string;
|
||||
};
|
||||
|
||||
export type ApiResponse = {
|
||||
error?: {
|
||||
message: string;
|
||||
};
|
||||
message?: string;
|
||||
model?: string;
|
||||
models?: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
}>;
|
||||
data?: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type StatusCheckResult = {
|
||||
status: ServiceStatus['status'];
|
||||
message?: string;
|
||||
incidents?: string[];
|
||||
responseTime?: number;
|
||||
};
|
||||
Reference in New Issue
Block a user