mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
feat(connectivityTest): add Codex connectivity test functionality and related state management
This commit is contained in:
@@ -63,6 +63,21 @@ export const buildOpenAIChatCompletionsEndpoint = (baseUrl: string): string => {
|
||||
return `${trimmed}/chat/completions`;
|
||||
};
|
||||
|
||||
export const buildCodexResponsesEndpoint = (baseUrl: string): string => {
|
||||
const trimmed = normalizeUpstreamBaseUrl(baseUrl);
|
||||
if (!trimmed) return '';
|
||||
if (/\/v1\/responses$/i.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
if (/\/v1\/models$/i.test(trimmed)) {
|
||||
return trimmed.replace(/\/models$/i, '/responses');
|
||||
}
|
||||
if (/\/v1$/i.test(trimmed)) {
|
||||
return `${trimmed}/responses`;
|
||||
}
|
||||
return `${trimmed}/v1/responses`;
|
||||
};
|
||||
|
||||
export const buildClaudeMessagesEndpoint = (baseUrl: string): string => {
|
||||
const trimmed = normalizeUpstreamBaseUrl(baseUrl, 'https://api.anthropic.com');
|
||||
if (!trimmed) return '';
|
||||
|
||||
@@ -54,7 +54,7 @@ export const PROVIDER_DESCRIPTORS: Record<ProviderBrand, ProviderDescriptor> = {
|
||||
supportsHeaders: true,
|
||||
supportsExcludedModels: true,
|
||||
supportsPriority: true,
|
||||
supportsTestModel: false,
|
||||
supportsTestModel: true,
|
||||
supportsWebsockets: true,
|
||||
supportsCloak: false,
|
||||
supportsApiKeyEntries: false,
|
||||
|
||||
@@ -86,7 +86,10 @@ function buildInitialForm(
|
||||
: undefined,
|
||||
experimentalCchSigning: brand === 'claude' ? false : undefined,
|
||||
testModel:
|
||||
brand === 'openaiCompatibility' || brand === 'claude' || brand === 'gemini'
|
||||
brand === 'openaiCompatibility' ||
|
||||
brand === 'codex' ||
|
||||
brand === 'claude' ||
|
||||
brand === 'gemini'
|
||||
? ''
|
||||
: undefined,
|
||||
apiKeyEntries: brand === 'openaiCompatibility' ? [emptyApiKeyEntry()] : undefined,
|
||||
@@ -173,7 +176,7 @@ function buildInitialForm(
|
||||
brand === 'claude'
|
||||
? (cfg as ProviderKeyConfig).experimentalCchSigning === true
|
||||
: undefined,
|
||||
testModel: brand === 'claude' || brand === 'gemini' ? '' : undefined,
|
||||
testModel: brand === 'codex' || brand === 'claude' || brand === 'gemini' ? '' : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -448,7 +451,9 @@ export function BaseProviderForm({
|
||||
brand === 'openaiCompatibility';
|
||||
const supportsOpenAIModelOptions = brand === 'openaiCompatibility';
|
||||
const singleConnectivity =
|
||||
brand === 'gemini'
|
||||
brand === 'codex'
|
||||
? { status: connectivity.codexStatus, run: connectivity.runCodex }
|
||||
: brand === 'gemini'
|
||||
? { status: connectivity.geminiStatus, run: connectivity.runGemini }
|
||||
: brand === 'claude'
|
||||
? { status: connectivity.claudeStatus, run: connectivity.runClaude }
|
||||
@@ -630,7 +635,7 @@ export function BaseProviderForm({
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor={`${fid}-testModel`}>
|
||||
{t('providersPage.form.testModel')}
|
||||
{brand === 'claude' || brand === 'gemini' ? (
|
||||
{brand === 'codex' || brand === 'claude' || brand === 'gemini' ? (
|
||||
<span className={styles.labelHint}>
|
||||
{' '}
|
||||
· {t('providersPage.form.testModelClaudeHint')}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { apiCallApi, getApiCallErrorMessage } from '@/services/api';
|
||||
import {
|
||||
buildCodexResponsesEndpoint,
|
||||
buildClaudeMessagesEndpoint,
|
||||
buildGeminiGenerateContentEndpoint,
|
||||
buildOpenAIChatCompletionsEndpoint,
|
||||
@@ -73,11 +74,13 @@ export interface ConnectivityErrorMessages {
|
||||
|
||||
export interface UseConnectivityTestResult {
|
||||
openaiStatuses: ConnectivityStatus[];
|
||||
codexStatus: ConnectivityStatus;
|
||||
geminiStatus: ConnectivityStatus;
|
||||
claudeStatus: ConnectivityStatus;
|
||||
isTestingAny: boolean;
|
||||
runOpenAIKey: (idx: number) => Promise<boolean>;
|
||||
runOpenAIAllKeys: () => Promise<void>;
|
||||
runCodex: () => Promise<void>;
|
||||
runGemini: () => Promise<void>;
|
||||
runClaude: () => Promise<void>;
|
||||
}
|
||||
@@ -103,6 +106,7 @@ export function useConnectivityTest(
|
||||
const [openaiStatuses, setOpenaiStatuses] = useState<ConnectivityStatus[]>(() =>
|
||||
Array.from({ length: entriesCount }, () => IDLE)
|
||||
);
|
||||
const [codexStatus, setCodexStatus] = useState<ConnectivityStatus>(IDLE);
|
||||
const [geminiStatus, setGeminiStatus] = useState<ConnectivityStatus>(IDLE);
|
||||
const [claudeStatus, setClaudeStatus] = useState<ConnectivityStatus>(IDLE);
|
||||
const [inFlight, setInFlight] = useState(0);
|
||||
@@ -160,6 +164,7 @@ export function useConnectivityTest(
|
||||
if (lastSignatureRef.current === signature) return;
|
||||
lastSignatureRef.current = signature;
|
||||
setOpenaiStatuses((prev) => prev.map(() => IDLE));
|
||||
setCodexStatus(IDLE);
|
||||
setGeminiStatus(IDLE);
|
||||
setClaudeStatus(IDLE);
|
||||
}, [signature]);
|
||||
@@ -277,6 +282,82 @@ export function useConnectivityTest(
|
||||
await Promise.all(entries.map((_, idx) => runOpenAIKey(idx)));
|
||||
}, [apiKeyEntries, brand, runOpenAIKey]);
|
||||
|
||||
const runCodex = useCallback(async (): Promise<void> => {
|
||||
if (brand !== 'codex') return;
|
||||
|
||||
const trimmedBase = baseUrl.trim();
|
||||
if (!trimmedBase) {
|
||||
setCodexStatus({ state: 'error', message: messages.baseUrlRequired });
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = buildCodexResponsesEndpoint(trimmedBase);
|
||||
if (!endpoint) {
|
||||
setCodexStatus({ state: 'error', message: messages.endpointInvalid });
|
||||
return;
|
||||
}
|
||||
|
||||
const model = pickModel(testModel, models);
|
||||
if (!model) {
|
||||
setCodexStatus({ state: 'error', message: messages.modelRequired });
|
||||
return;
|
||||
}
|
||||
|
||||
const customHeaders = buildHeaderObject(formHeaders);
|
||||
const explicitKey = (apiKey ?? '').trim();
|
||||
const persistedKey = (fallbackApiKey ?? '').trim();
|
||||
const hasAuthorization = hasHeader(customHeaders, 'authorization');
|
||||
const resolvedKey = explicitKey || persistedKey;
|
||||
const resolvedAuthIndex = (authIndex ?? '').trim() || undefined;
|
||||
|
||||
if (!resolvedKey && !hasAuthorization && !resolvedAuthIndex) {
|
||||
setCodexStatus({ state: 'error', message: messages.apiKeyRequired });
|
||||
return;
|
||||
}
|
||||
|
||||
const headerObj: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...customHeaders,
|
||||
};
|
||||
if (!hasHeader(headerObj, 'authorization')) {
|
||||
if (resolvedKey) {
|
||||
headerObj.Authorization = `Bearer ${resolvedKey}`;
|
||||
} else if (resolvedAuthIndex) {
|
||||
headerObj.Authorization = 'Bearer $TOKEN$';
|
||||
}
|
||||
}
|
||||
|
||||
setCodexStatus({ state: 'loading', message: '' });
|
||||
setInFlight((n) => n + 1);
|
||||
try {
|
||||
const result = await apiCallApi.request(
|
||||
{
|
||||
authIndex: resolvedAuthIndex,
|
||||
method: 'POST',
|
||||
url: endpoint,
|
||||
header: headerObj,
|
||||
data: JSON.stringify({
|
||||
model,
|
||||
input: 'Hi',
|
||||
stream: false,
|
||||
}),
|
||||
},
|
||||
{ timeout: DEFAULT_TIMEOUT_MS }
|
||||
);
|
||||
if (result.statusCode < 200 || result.statusCode >= 300) {
|
||||
throw new Error(getApiCallErrorMessage(result));
|
||||
}
|
||||
setCodexStatus({ state: 'success', message: '' });
|
||||
} catch (err) {
|
||||
setCodexStatus({
|
||||
state: 'error',
|
||||
message: requestFailureMessage(err, messages),
|
||||
});
|
||||
} finally {
|
||||
setInFlight((n) => n - 1);
|
||||
}
|
||||
}, [apiKey, authIndex, baseUrl, brand, fallbackApiKey, formHeaders, messages, models, testModel]);
|
||||
|
||||
const runGemini = useCallback(async (): Promise<void> => {
|
||||
if (brand !== 'gemini') return;
|
||||
|
||||
@@ -419,11 +500,13 @@ export function useConnectivityTest(
|
||||
|
||||
return {
|
||||
openaiStatuses,
|
||||
codexStatus,
|
||||
geminiStatus,
|
||||
claudeStatus,
|
||||
isTestingAny: inFlight > 0,
|
||||
runOpenAIKey,
|
||||
runOpenAIAllKeys,
|
||||
runCodex,
|
||||
runGemini,
|
||||
runClaude,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user