fix custom provider API key validation fallback (#773)

Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
This commit is contained in:
Lingxuan Zuo
2026-04-06 14:35:09 +08:00
committed by GitHub
Unverified
parent e640316382
commit 91c735c9f4
8 changed files with 554 additions and 80 deletions

View File

@@ -10,6 +10,10 @@ type ValidationProfile =
| 'none';
type ValidationResult = { valid: boolean; error?: string; status?: number };
type ClassifiedValidationResult = ValidationResult & { authFailure?: boolean };
const AUTH_ERROR_PATTERN = /\b(unauthorized|forbidden|access denied|invalid api key|api key invalid|incorrect api key|api key incorrect|authentication failed|auth failed|invalid credential|credential invalid|invalid signature|signature invalid|invalid access token|access token invalid|invalid bearer token|bearer token invalid|access token expired)\b|鉴权失败|認証失敗|认证失败|無效密鑰|无效密钥|密钥无效|密鑰無效|憑證無效|凭证无效/i;
const AUTH_ERROR_CODE_PATTERN = /\b(unauthorized|forbidden|access[_-]?denied|invalid[_-]?api[_-]?key|api[_-]?key[_-]?invalid|incorrect[_-]?api[_-]?key|api[_-]?key[_-]?incorrect|authentication[_-]?failed|auth[_-]?failed|invalid[_-]?credential|credential[_-]?invalid|invalid[_-]?signature|signature[_-]?invalid|invalid[_-]?access[_-]?token|access[_-]?token[_-]?invalid|invalid[_-]?bearer[_-]?token|bearer[_-]?token[_-]?invalid|access[_-]?token[_-]?expired|invalid[_-]?token|token[_-]?invalid|token[_-]?expired)\b/i;
function logValidationStatus(provider: string, status: number): void {
console.log(`[clawx-validate] ${provider} HTTP ${status}`);
@@ -118,7 +122,7 @@ async function performProviderValidationRequest(
providerLabel: string,
url: string,
headers: Record<string, string>,
): Promise<ValidationResult> {
): Promise<ClassifiedValidationResult> {
try {
logValidationRequest(providerLabel, 'GET', url, headers);
const response = await proxyAwareFetch(url, { headers });
@@ -137,16 +141,56 @@ async function performProviderValidationRequest(
function classifyAuthResponse(
status: number,
data: unknown,
): { valid: boolean; error?: string } {
) : ClassifiedValidationResult {
const obj = data as {
error?: { message?: string; code?: string };
message?: string;
code?: string;
} | null;
const msg = obj?.error?.message || obj?.message || `API error: ${status}`;
const code = obj?.error?.code || obj?.code;
const hasAuthCode = typeof code === 'string' && AUTH_ERROR_CODE_PATTERN.test(code);
if (status >= 200 && status < 300) return { valid: true };
if (status === 429) return { valid: true };
if (status === 401 || status === 403) return { valid: false, error: 'Invalid API key' };
if (status === 401 || status === 403) {
return { valid: false, error: 'Invalid API key', authFailure: true };
}
if (status === 400 && (AUTH_ERROR_PATTERN.test(msg) || hasAuthCode)) {
const error = hasAuthCode && msg === `API error: ${status}`
? `Invalid API key (${code})`
: msg || 'Invalid API key';
return { valid: false, error, authFailure: true };
}
const obj = data as { error?: { message?: string }; message?: string } | null;
const msg = obj?.error?.message || obj?.message || `API error: ${status}`;
return { valid: false, error: msg };
}
function shouldFallbackFromModelsProbe(result: ClassifiedValidationResult): boolean {
if (result.valid || result.status === undefined) return false;
if (result.status === 401 || result.status === 403) return false;
if (result.authFailure) return false;
return true;
}
function classifyProbeResponse(
status: number,
data: unknown,
): ClassifiedValidationResult {
const classified = classifyAuthResponse(status, data);
if (status >= 200 && status < 300) {
return { valid: true, status };
}
if (status === 429) {
return { valid: true, status };
}
if (status === 400 && !classified.authFailure) {
return { valid: true, status };
}
return { ...classified, status };
}
async function validateOpenAiCompatibleKey(
providerType: string,
apiKey: string,
@@ -162,9 +206,9 @@ async function validateOpenAiCompatibleKey(
const { modelsUrl, probeUrl } = resolveOpenAiProbeUrls(trimmedBaseUrl, apiProtocol);
const modelsResult = await performProviderValidationRequest(providerType, modelsUrl, headers);
if (modelsResult.status === 404) {
if (shouldFallbackFromModelsProbe(modelsResult)) {
console.log(
`[clawx-validate] ${providerType} /models returned 404, falling back to ${apiProtocol} probe`,
`[clawx-validate] ${providerType} /models returned ${modelsResult.status}, falling back to ${apiProtocol} probe`,
);
if (apiProtocol === 'openai-responses') {
return await performResponsesProbe(providerType, probeUrl, headers);
@@ -192,18 +236,7 @@ async function performResponsesProbe(
});
logValidationStatus(providerLabel, response.status);
const data = await response.json().catch(() => ({}));
if (response.status === 401 || response.status === 403) {
return { valid: false, error: 'Invalid API key' };
}
if (
(response.status >= 200 && response.status < 300) ||
response.status === 400 ||
response.status === 429
) {
return { valid: true };
}
return classifyAuthResponse(response.status, data);
return classifyProbeResponse(response.status, data);
} catch (error) {
return {
valid: false,
@@ -230,18 +263,7 @@ async function performChatCompletionsProbe(
});
logValidationStatus(providerLabel, response.status);
const data = await response.json().catch(() => ({}));
if (response.status === 401 || response.status === 403) {
return { valid: false, error: 'Invalid API key' };
}
if (
(response.status >= 200 && response.status < 300) ||
response.status === 400 ||
response.status === 429
) {
return { valid: true };
}
return classifyAuthResponse(response.status, data);
return classifyProbeResponse(response.status, data);
} catch (error) {
return {
valid: false,
@@ -268,18 +290,7 @@ async function performAnthropicMessagesProbe(
});
logValidationStatus(providerLabel, response.status);
const data = await response.json().catch(() => ({}));
if (response.status === 401 || response.status === 403) {
return { valid: false, error: 'Invalid API key' };
}
if (
(response.status >= 200 && response.status < 300) ||
response.status === 400 ||
response.status === 429
) {
return { valid: true };
}
return classifyAuthResponse(response.status, data);
return classifyProbeResponse(response.status, data);
} catch (error) {
return {
valid: false,