fix minimax cn api key 401 error (#396)
This commit is contained in:
committed by
GitHub
Unverified
parent
45d7ff61c3
commit
880995af19
@@ -24,6 +24,22 @@ type RuntimeProviderSyncContext = {
|
|||||||
api: string;
|
api: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeProviderBaseUrl(config: ProviderConfig, baseUrl?: string): string | undefined {
|
||||||
|
if (!baseUrl) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.type === 'minimax-portal' || config.type === 'minimax-portal-cn') {
|
||||||
|
return baseUrl.replace(/\/v1$/, '').replace(/\/anthropic$/, '').replace(/\/$/, '') + '/anthropic';
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUseExplicitDefaultOverride(config: ProviderConfig, runtimeProviderKey: string): boolean {
|
||||||
|
return Boolean(config.baseUrl || config.apiProtocol || runtimeProviderKey !== config.type);
|
||||||
|
}
|
||||||
|
|
||||||
export function getOpenClawProviderKey(type: string, providerId: string): string {
|
export function getOpenClawProviderKey(type: string, providerId: string): string {
|
||||||
if (type === 'custom' || type === 'ollama') {
|
if (type === 'custom' || type === 'ollama') {
|
||||||
const suffix = providerId.replace(/-/g, '').slice(0, 8);
|
const suffix = providerId.replace(/-/g, '').slice(0, 8);
|
||||||
@@ -233,7 +249,7 @@ async function syncRuntimeProviderConfig(
|
|||||||
context: RuntimeProviderSyncContext,
|
context: RuntimeProviderSyncContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await syncProviderConfigToOpenClaw(context.runtimeProviderKey, config.model, {
|
await syncProviderConfigToOpenClaw(context.runtimeProviderKey, config.model, {
|
||||||
baseUrl: config.baseUrl || context.meta?.baseUrl,
|
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl || context.meta?.baseUrl),
|
||||||
api: context.api,
|
api: context.api,
|
||||||
apiKeyEnv: context.meta?.apiKeyEnv,
|
apiKeyEnv: context.meta?.apiKeyEnv,
|
||||||
headers: context.meta?.headers,
|
headers: context.meta?.headers,
|
||||||
@@ -311,7 +327,16 @@ export async function syncUpdatedProviderToRuntime(
|
|||||||
if (defaultProviderId === config.id) {
|
if (defaultProviderId === config.id) {
|
||||||
const modelOverride = config.model ? `${ock}/${config.model}` : undefined;
|
const modelOverride = config.model ? `${ock}/${config.model}` : undefined;
|
||||||
if (config.type !== 'custom') {
|
if (config.type !== 'custom') {
|
||||||
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
if (shouldUseExplicitDefaultOverride(config, ock)) {
|
||||||
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||||
|
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl || context.meta?.baseUrl),
|
||||||
|
api: context.api,
|
||||||
|
apiKeyEnv: context.meta?.apiKeyEnv,
|
||||||
|
headers: context.meta?.headers,
|
||||||
|
}, fallbackModels);
|
||||||
|
} else {
|
||||||
|
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||||
baseUrl: config.baseUrl,
|
baseUrl: config.baseUrl,
|
||||||
@@ -384,6 +409,13 @@ export async function syncDefaultProviderToRuntime(
|
|||||||
baseUrl: provider.baseUrl,
|
baseUrl: provider.baseUrl,
|
||||||
api: provider.apiProtocol || 'openai-completions',
|
api: provider.apiProtocol || 'openai-completions',
|
||||||
}, fallbackModels);
|
}, fallbackModels);
|
||||||
|
} else if (shouldUseExplicitDefaultOverride(provider, ock)) {
|
||||||
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||||
|
baseUrl: normalizeProviderBaseUrl(provider, provider.baseUrl || getProviderConfig(provider.type)?.baseUrl),
|
||||||
|
api: provider.apiProtocol || getProviderConfig(provider.type)?.api,
|
||||||
|
apiKeyEnv: getProviderConfig(provider.type)?.apiKeyEnv,
|
||||||
|
headers: getProviderConfig(provider.type)?.headers,
|
||||||
|
}, fallbackModels);
|
||||||
} else {
|
} else {
|
||||||
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { proxyAwareFetch } from '../../utils/proxy-fetch';
|
import { proxyAwareFetch } from '../../utils/proxy-fetch';
|
||||||
|
import { getProviderConfig } from '../../utils/provider-registry';
|
||||||
|
|
||||||
type ValidationProfile =
|
type ValidationProfile =
|
||||||
| 'openai-compatible'
|
| 'openai-compatible'
|
||||||
@@ -59,7 +60,18 @@ function logValidationRequest(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValidationProfile(providerType: string): ValidationProfile {
|
function getValidationProfile(
|
||||||
|
providerType: string,
|
||||||
|
options?: { apiProtocol?: string }
|
||||||
|
): ValidationProfile {
|
||||||
|
const providerApi = options?.apiProtocol || getProviderConfig(providerType)?.api;
|
||||||
|
if (providerApi === 'anthropic-messages') {
|
||||||
|
return 'anthropic-header';
|
||||||
|
}
|
||||||
|
if (providerApi === 'openai-completions' || providerApi === 'openai-responses') {
|
||||||
|
return 'openai-compatible';
|
||||||
|
}
|
||||||
|
|
||||||
switch (providerType) {
|
switch (providerType) {
|
||||||
case 'anthropic':
|
case 'anthropic':
|
||||||
return 'anthropic-header';
|
return 'anthropic-header';
|
||||||
@@ -259,15 +271,8 @@ export async function validateApiKeyWithProvider(
|
|||||||
apiKey: string,
|
apiKey: string,
|
||||||
options?: { baseUrl?: string; apiProtocol?: string },
|
options?: { baseUrl?: string; apiProtocol?: string },
|
||||||
): Promise<{ valid: boolean; error?: string }> {
|
): Promise<{ valid: boolean; error?: string }> {
|
||||||
let profile = getValidationProfile(providerType);
|
const profile = getValidationProfile(providerType, options);
|
||||||
|
const resolvedBaseUrl = options?.baseUrl || getProviderConfig(providerType)?.baseUrl;
|
||||||
if (providerType === 'custom' && options?.apiProtocol) {
|
|
||||||
if (options.apiProtocol === 'anthropic-messages') {
|
|
||||||
profile = 'anthropic-header';
|
|
||||||
} else {
|
|
||||||
profile = 'openai-compatible';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile === 'none') {
|
if (profile === 'none') {
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
@@ -281,11 +286,11 @@ export async function validateApiKeyWithProvider(
|
|||||||
try {
|
try {
|
||||||
switch (profile) {
|
switch (profile) {
|
||||||
case 'openai-compatible':
|
case 'openai-compatible':
|
||||||
return await validateOpenAiCompatibleKey(providerType, trimmedKey, options?.baseUrl);
|
return await validateOpenAiCompatibleKey(providerType, trimmedKey, resolvedBaseUrl);
|
||||||
case 'google-query-key':
|
case 'google-query-key':
|
||||||
return await validateGoogleQueryKey(providerType, trimmedKey, options?.baseUrl);
|
return await validateGoogleQueryKey(providerType, trimmedKey, resolvedBaseUrl);
|
||||||
case 'anthropic-header':
|
case 'anthropic-header':
|
||||||
return await validateAnthropicHeaderKey(providerType, trimmedKey, options?.baseUrl);
|
return await validateAnthropicHeaderKey(providerType, trimmedKey, resolvedBaseUrl);
|
||||||
case 'openrouter':
|
case 'openrouter':
|
||||||
return await validateOpenRouterKey(providerType, trimmedKey);
|
return await validateOpenRouterKey(providerType, trimmedKey);
|
||||||
default:
|
default:
|
||||||
|
|||||||
52
tests/unit/provider-validation.test.ts
Normal file
52
tests/unit/provider-validation.test.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const proxyAwareFetch = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('@electron/utils/proxy-fetch', () => ({
|
||||||
|
proxyAwareFetch,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('validateApiKeyWithProvider', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
proxyAwareFetch.mockReset();
|
||||||
|
proxyAwareFetch.mockResolvedValue(
|
||||||
|
new Response(JSON.stringify({ data: [] }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates MiniMax CN keys with Anthropic headers', async () => {
|
||||||
|
const { validateApiKeyWithProvider } = await import('@electron/services/providers/provider-validation');
|
||||||
|
|
||||||
|
const result = await validateApiKeyWithProvider('minimax-portal-cn', 'sk-cn-test');
|
||||||
|
|
||||||
|
expect(result).toEqual({ valid: true });
|
||||||
|
expect(proxyAwareFetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.minimaxi.com/anthropic/v1/models?limit=1',
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
'x-api-key': 'sk-cn-test',
|
||||||
|
'anthropic-version': '2023-06-01',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still validates OpenAI-compatible providers with bearer auth', async () => {
|
||||||
|
const { validateApiKeyWithProvider } = await import('@electron/services/providers/provider-validation');
|
||||||
|
|
||||||
|
const result = await validateApiKeyWithProvider('openai', 'sk-openai-test');
|
||||||
|
|
||||||
|
expect(result).toEqual({ valid: true });
|
||||||
|
expect(proxyAwareFetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.openai.com/v1/models?limit=1',
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
Authorization: 'Bearer sk-openai-test',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user