fix(providers): forward stored apiKey and authIndex to discovery and tests

The form's apiKey field is intentionally empty in edit mode (the
placeholder reads "leave blank to keep unchanged"), so model discovery
and the Claude connectivity test were sending requests with no
credentials and getting 401 for Codex / Claude / Gemini providers.

- BaseProviderForm derives a fallbackAuthIndex from resource.raw.authIndex
  alongside the existing fallbackApiKey and passes both into the
  discovery and connectivity hooks
- modelsApi.fetch{V1,Claude,Gemini,}ModelsViaApiCall accept an optional
  authIndex and forward it on apiCallApi.request so the backend can
  inject the OAuth/auth-file token for providers that store the key
  externally
- useModelDiscovery threads the authIndex through to every brand
- useConnectivityTest threads authIndex through the Claude path and no
  longer rejects the request as "API key required" when an authIndex is
  available
This commit is contained in:
LTbinglingfeng
2026-05-25 02:24:49 +08:00
parent 87ddd62c40
commit 7e9c5be640
4 changed files with 56 additions and 11 deletions
@@ -226,6 +226,13 @@ export function BaseProviderForm({
return (resource.raw as { apiKey?: string } | undefined)?.apiKey ?? '';
}, [brand, mode, resource]);
const fallbackAuthIndex = useMemo(() => {
if (mode !== 'edit' || !resource) return '';
return (
(resource.raw as { authIndex?: string } | undefined)?.authIndex ?? ''
);
}, [mode, resource]);
const connectivityMessages = useMemo<ConnectivityErrorMessages>(
() => ({
baseUrlRequired: t('providersPage.connectivity.baseUrlRequired'),
@@ -249,6 +256,7 @@ export function BaseProviderForm({
apiKeyEntries: form.apiKeyEntries,
apiKey: form.apiKey,
fallbackApiKey,
authIndex: fallbackAuthIndex,
},
connectivityMessages
);
@@ -260,6 +268,7 @@ export function BaseProviderForm({
apiKeyEntries: form.apiKeyEntries,
apiKey: form.apiKey,
fallbackApiKey,
authIndex: fallbackAuthIndex,
});
const [discoveryOpen, setDiscoveryOpen] = useState(false);
@@ -77,6 +77,7 @@ export interface UseConnectivityTestArgs {
apiKeyEntries?: ApiKeyEntryInput[];
apiKey?: string;
fallbackApiKey?: string;
authIndex?: string;
}
export interface ConnectivityErrorMessages {
@@ -110,6 +111,7 @@ export function useConnectivityTest(
apiKeyEntries,
apiKey,
fallbackApiKey,
authIndex,
} = args;
const entriesCount = apiKeyEntries?.length ?? 0;
@@ -302,8 +304,9 @@ export function useConnectivityTest(
const headerKey = resolveBearerToken(customHeaders);
const hasApiKeyHeader = hasHeader(customHeaders, 'x-api-key');
const resolvedKey = explicitKey || persistedKey || headerKey;
const resolvedAuthIndex = (authIndex ?? '').trim() || undefined;
if (!resolvedKey && !hasApiKeyHeader) {
if (!resolvedKey && !hasApiKeyHeader && !resolvedAuthIndex) {
setClaudeStatus({ state: 'error', message: messages.apiKeyRequired });
return;
}
@@ -324,6 +327,7 @@ export function useConnectivityTest(
try {
const result = await apiCallApi.request(
{
authIndex: resolvedAuthIndex,
method: 'POST',
url: endpoint,
header: headerObj,
@@ -358,6 +362,7 @@ export function useConnectivityTest(
}
}, [
apiKey,
authIndex,
baseUrl,
brand,
fallbackApiKey,
@@ -44,6 +44,7 @@ export interface UseModelDiscoveryArgs {
apiKeyEntries?: ApiKeyEntryInput[];
apiKey?: string;
fallbackApiKey?: string;
authIndex?: string;
}
export interface UseModelDiscoveryResult {
@@ -59,8 +60,15 @@ export interface UseModelDiscoveryResult {
export function useModelDiscovery(
args: UseModelDiscoveryArgs
): UseModelDiscoveryResult {
const { brand, baseUrl, formHeaders, apiKeyEntries, apiKey, fallbackApiKey } =
args;
const {
brand,
baseUrl,
formHeaders,
apiKeyEntries,
apiKey,
fallbackApiKey,
authIndex,
} = args;
const available = isModelDiscoveryBrand(brand);
const [loading, setLoading] = useState(false);
@@ -74,27 +82,31 @@ export function useModelDiscovery(
setError(null);
try {
const baseHeaders = buildHeaderObject(formHeaders);
const resolvedAuthIndex = (authIndex ?? '').trim() || undefined;
let next: ModelInfo[] = [];
if (brand === 'gemini') {
const key = (apiKey ?? '').trim() || (fallbackApiKey ?? '').trim();
next = await modelsApi.fetchGeminiModelsViaApiCall(
baseUrl,
key,
baseHeaders
baseHeaders,
resolvedAuthIndex
);
} else if (brand === 'codex') {
const key = (apiKey ?? '').trim() || (fallbackApiKey ?? '').trim();
next = await modelsApi.fetchV1ModelsViaApiCall(
baseUrl,
key,
baseHeaders
baseHeaders,
resolvedAuthIndex
);
} else if (brand === 'claude') {
const key = (apiKey ?? '').trim() || (fallbackApiKey ?? '').trim();
next = await modelsApi.fetchClaudeModelsViaApiCall(
baseUrl,
key,
baseHeaders
baseHeaders,
resolvedAuthIndex
);
} else if (brand === 'openaiCompatibility') {
const firstEntry = (apiKeyEntries ?? []).find((e) =>
@@ -129,7 +141,16 @@ export function useModelDiscovery(
} finally {
setLoading(false);
}
}, [available, apiKey, apiKeyEntries, baseUrl, brand, fallbackApiKey, formHeaders]);
}, [
available,
apiKey,
apiKeyEntries,
authIndex,
baseUrl,
brand,
fallbackApiKey,
formHeaders,
]);
const reset = useCallback(() => {
setModels([]);
+14 -4
View File
@@ -108,7 +108,8 @@ export const modelsApi = {
async fetchV1ModelsViaApiCall(
baseUrl: string,
apiKey?: string,
headers: Record<string, string> = {}
headers: Record<string, string> = {},
authIndex?: string
) {
const endpoint = buildV1ModelsEndpoint(baseUrl);
if (!endpoint) {
@@ -121,6 +122,7 @@ export const modelsApi = {
}
const result = await apiCallApi.request({
authIndex: authIndex?.trim() || undefined,
method: 'GET',
url: endpoint,
header: Object.keys(resolvedHeaders).length ? resolvedHeaders : undefined
@@ -140,7 +142,8 @@ export const modelsApi = {
async fetchModelsViaApiCall(
baseUrl: string,
apiKey?: string,
headers: Record<string, string> = {}
headers: Record<string, string> = {},
authIndex?: string
) {
const endpoint = buildModelsEndpoint(baseUrl);
if (!endpoint) {
@@ -153,6 +156,7 @@ export const modelsApi = {
}
const result = await apiCallApi.request({
authIndex: authIndex?.trim() || undefined,
method: 'GET',
url: endpoint,
header: Object.keys(resolvedHeaders).length ? resolvedHeaders : undefined
@@ -185,7 +189,8 @@ export const modelsApi = {
async fetchClaudeModelsViaApiCall(
baseUrl: string,
apiKey?: string,
headers: Record<string, string> = {}
headers: Record<string, string> = {},
authIndex?: string
) {
const endpoint = buildClaudeModelsEndpoint(baseUrl);
if (!endpoint) {
@@ -205,12 +210,14 @@ export const modelsApi = {
resolvedHeaders['anthropic-version'] = DEFAULT_ANTHROPIC_VERSION;
}
const trimmedAuthIndex = authIndex?.trim() || undefined;
const signature = buildRequestSignature(endpoint, resolvedHeaders);
const existing = CLAUDE_MODELS_IN_FLIGHT.get(signature);
if (existing) return existing;
const request = (async () => {
const result = await apiCallApi.request({
authIndex: trimmedAuthIndex,
method: 'GET',
url: endpoint,
header: Object.keys(resolvedHeaders).length ? resolvedHeaders : undefined
@@ -239,7 +246,8 @@ export const modelsApi = {
async fetchGeminiModelsViaApiCall(
baseUrl: string,
apiKey?: string,
headers: Record<string, string> = {}
headers: Record<string, string> = {},
authIndex?: string
) {
const endpoint = buildGeminiModelsEndpoint(baseUrl);
if (!endpoint) {
@@ -252,6 +260,7 @@ export const modelsApi = {
resolvedHeaders['x-goog-api-key'] = resolvedApiKey;
}
const trimmedAuthIndex = authIndex?.trim() || undefined;
const signature = buildRequestSignature(endpoint, resolvedHeaders);
const existing = GEMINI_MODELS_IN_FLIGHT.get(signature);
if (existing) return existing;
@@ -268,6 +277,7 @@ export const modelsApi = {
}
const result = await apiCallApi.request({
authIndex: trimmedAuthIndex,
method: 'GET',
url: url.toString(),
header: Object.keys(resolvedHeaders).length ? resolvedHeaders : undefined