From 7e9c5be6408d971f3777ce4fec566f37589f5d81 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Mon, 25 May 2026 02:24:49 +0800 Subject: [PATCH] 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 --- .../sheets/forms/BaseProviderForm.tsx | 9 +++++ .../sheets/forms/useConnectivityTest.ts | 7 +++- .../sheets/forms/useModelDiscovery.ts | 33 +++++++++++++++---- src/services/api/models.ts | 18 +++++++--- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/features/providers/sheets/forms/BaseProviderForm.tsx b/src/features/providers/sheets/forms/BaseProviderForm.tsx index 8290634..b025e70 100644 --- a/src/features/providers/sheets/forms/BaseProviderForm.tsx +++ b/src/features/providers/sheets/forms/BaseProviderForm.tsx @@ -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( () => ({ 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); diff --git a/src/features/providers/sheets/forms/useConnectivityTest.ts b/src/features/providers/sheets/forms/useConnectivityTest.ts index 900802c..b8c2f8d 100644 --- a/src/features/providers/sheets/forms/useConnectivityTest.ts +++ b/src/features/providers/sheets/forms/useConnectivityTest.ts @@ -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, diff --git a/src/features/providers/sheets/forms/useModelDiscovery.ts b/src/features/providers/sheets/forms/useModelDiscovery.ts index 5014b7c..874b5cb 100644 --- a/src/features/providers/sheets/forms/useModelDiscovery.ts +++ b/src/features/providers/sheets/forms/useModelDiscovery.ts @@ -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([]); diff --git a/src/services/api/models.ts b/src/services/api/models.ts index b18886d..9977d5b 100644 --- a/src/services/api/models.ts +++ b/src/services/api/models.ts @@ -108,7 +108,8 @@ export const modelsApi = { async fetchV1ModelsViaApiCall( baseUrl: string, apiKey?: string, - headers: Record = {} + headers: Record = {}, + 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 = {} + headers: Record = {}, + 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 = {} + headers: Record = {}, + 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 = {} + headers: Record = {}, + 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