feat(connectivityTest): add Codex connectivity test functionality and related state management

This commit is contained in:
LTbinglingfeng
2026-06-15 01:40:31 +08:00
Unverified
parent 246069d128
commit 6442c92784
4 changed files with 108 additions and 5 deletions
+15
View File
@@ -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 '';
+1 -1
View File
@@ -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,
};