chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
const DEFAULT_TIMEOUT_MS = 15000;
|
||||
|
||||
const PROVIDER_TEST_CONFIGS = {
|
||||
openai: {
|
||||
format: "openai",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
model: "gpt-4o-mini",
|
||||
},
|
||||
openrouter: {
|
||||
format: "openai",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
model: "openai/gpt-4o-mini",
|
||||
},
|
||||
groq: {
|
||||
format: "openai",
|
||||
baseUrl: "https://api.groq.com/openai/v1",
|
||||
model: "llama-3.1-8b-instant",
|
||||
},
|
||||
mistral: {
|
||||
format: "openai",
|
||||
baseUrl: "https://api.mistral.ai/v1",
|
||||
model: "mistral-small-latest",
|
||||
},
|
||||
anthropic: {
|
||||
format: "anthropic",
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
model: "claude-3-5-haiku-latest",
|
||||
},
|
||||
google: {
|
||||
format: "google",
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
model: "gemini-1.5-flash",
|
||||
},
|
||||
};
|
||||
|
||||
function joinUrl(baseUrl, suffix) {
|
||||
return `${baseUrl.replace(/\/+$/, "")}/${suffix.replace(/^\/+/, "")}`;
|
||||
}
|
||||
|
||||
function providerEnvName(provider, suffix) {
|
||||
const normalizedProvider = String(provider || "")
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9]/g, "_");
|
||||
return `OMNIROUTE_PROVIDER_TEST_${normalizedProvider}_${suffix}`;
|
||||
}
|
||||
|
||||
function resolveTestModel(input, config) {
|
||||
const providerOverride = process.env[providerEnvName(input.provider, "MODEL")];
|
||||
return (
|
||||
input.defaultModel ||
|
||||
providerOverride ||
|
||||
process.env.OMNIROUTE_PROVIDER_TEST_MODEL ||
|
||||
config.model
|
||||
);
|
||||
}
|
||||
|
||||
function resolveProviderConfig(input) {
|
||||
const config = PROVIDER_TEST_CONFIGS[input.provider];
|
||||
if (!config) return null;
|
||||
|
||||
return {
|
||||
...config,
|
||||
baseUrl: input.baseUrl || config.baseUrl,
|
||||
model: resolveTestModel(input, config),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, init = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
return await fetch(url, {
|
||||
...init,
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function classifyResponse(response) {
|
||||
if (response.ok) return { valid: true, error: null, statusCode: response.status };
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return { valid: false, error: "Invalid API key", statusCode: response.status };
|
||||
}
|
||||
if (response.status >= 500) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Provider unavailable (${response.status})`,
|
||||
statusCode: response.status,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true, error: null, statusCode: response.status };
|
||||
}
|
||||
|
||||
async function testOpenAILikeProvider(input, config) {
|
||||
const headers = {
|
||||
Authorization: `Bearer ${input.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
const modelsRes = await fetchWithTimeout(joinUrl(config.baseUrl, "/models"), {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (modelsRes.ok || modelsRes.status === 401 || modelsRes.status === 403) {
|
||||
return classifyResponse(modelsRes);
|
||||
}
|
||||
|
||||
const chatRes = await fetchWithTimeout(joinUrl(config.baseUrl, "/chat/completions"), {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
model: config.model,
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
max_tokens: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
return classifyResponse(chatRes);
|
||||
}
|
||||
|
||||
async function testAnthropicProvider(input, config) {
|
||||
const response = await fetchWithTimeout(joinUrl(config.baseUrl, "/messages"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-api-key": input.apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config.model,
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
max_tokens: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
return classifyResponse(response);
|
||||
}
|
||||
|
||||
async function testGoogleProvider(input, config) {
|
||||
const url = new URL(joinUrl(config.baseUrl, "/models"));
|
||||
url.searchParams.set("key", input.apiKey);
|
||||
|
||||
const response = await fetchWithTimeout(url.toString(), {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return classifyResponse(response);
|
||||
}
|
||||
|
||||
export async function testProviderApiKey(input) {
|
||||
if (!input.apiKey) {
|
||||
return { valid: false, error: "Missing API key", statusCode: null };
|
||||
}
|
||||
|
||||
const config = resolveProviderConfig(input);
|
||||
if (!config) {
|
||||
return { valid: false, error: "Provider test not supported", unsupported: true };
|
||||
}
|
||||
|
||||
try {
|
||||
if (config.format === "openai") {
|
||||
return await testOpenAILikeProvider(input, config);
|
||||
}
|
||||
if (config.format === "anthropic") {
|
||||
return await testAnthropicProvider(input, config);
|
||||
}
|
||||
if (config.format === "google") {
|
||||
return await testGoogleProvider(input, config);
|
||||
}
|
||||
|
||||
return { valid: false, error: "Provider test not supported", unsupported: true };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return { valid: false, error: message || "Provider test failed", statusCode: null };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user