Improve first-run and channel setup for non-technical users: detect newer Gemini key formats, pin Node 22.22.3, add Control Panel Telegram approve flow, and keep channels/models when config is rewritten. Persist uclaw wizard state via uclaw-meta.json so restarts skip language/persona prompts. Co-authored-by: Cursor <cursoragent@cursor.com>
119 lines
4.5 KiB
JavaScript
119 lines
4.5 KiB
JavaScript
// Works out which provider an API key belongs to, so users never have to know
|
|
// what a Base URL is.
|
|
//
|
|
// The old flow asked for three things — Base URL, model name and key — on the
|
|
// first screen a non-technical user ever saw. Every key in practice announces
|
|
// its own provider through its prefix, so ask for the key and derive the rest.
|
|
|
|
// Order matters: the longest, most specific prefixes are checked first, because
|
|
// several providers issue keys that also start with "sk-".
|
|
const PROVIDERS = [
|
|
{
|
|
id: 'anthropic',
|
|
label: 'Anthropic Claude',
|
|
prefixes: ['sk-ant-'],
|
|
baseUrl: 'https://api.anthropic.com/v1',
|
|
model: 'claude-sonnet-4-20250514',
|
|
models: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-haiku-4-5-20251001'],
|
|
},
|
|
{
|
|
id: 'openrouter',
|
|
label: 'OpenRouter',
|
|
prefixes: ['sk-or-v1-', 'sk-or-'],
|
|
baseUrl: 'https://openrouter.ai/api/v1',
|
|
model: 'anthropic/claude-sonnet-4',
|
|
models: ['anthropic/claude-sonnet-4', 'openai/gpt-4o', 'google/gemini-2.5-flash'],
|
|
},
|
|
{
|
|
id: 'google',
|
|
label: 'Google Gemini',
|
|
// Google AI Studio now issues Auth keys (AQ.Ab…) alongside legacy Standard keys (AIzaSy…).
|
|
prefixes: ['AQ.', 'AIza'],
|
|
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
// New AI Studio accounts often cannot use 2.5-flash yet (404). Try 2.0 first.
|
|
model: 'gemini-2.0-flash',
|
|
models: ['gemini-2.0-flash', 'gemini-flash-latest', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-pro'],
|
|
},
|
|
{
|
|
id: 'groq',
|
|
label: 'Groq',
|
|
prefixes: ['gsk_'],
|
|
baseUrl: 'https://api.groq.com/openai/v1',
|
|
model: 'llama-3.3-70b-versatile',
|
|
models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant'],
|
|
},
|
|
{
|
|
id: 'sealion',
|
|
label: 'SEA-LION',
|
|
prefixes: ['sk-sealion-'],
|
|
baseUrl: 'https://api.sea-lion.ai/v1',
|
|
model: 'aisingapore/Qwen-SEA-LION-v4.5-27B-IT',
|
|
models: ['aisingapore/Qwen-SEA-LION-v4.5-27B-IT', 'aisingapore/Llama-SEA-LION-v3.5-70B-R'],
|
|
},
|
|
{
|
|
id: 'openai',
|
|
label: 'OpenAI',
|
|
prefixes: ['sk-proj-', 'sk-svcacct-', 'sk-'],
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
model: 'gpt-4o',
|
|
models: ['gpt-4o', 'gpt-4o-mini', 'o3-mini'],
|
|
},
|
|
];
|
|
|
|
export function detectProvider(rawKey) {
|
|
const key = String(rawKey ?? '').trim();
|
|
if (!key) return null;
|
|
for (const provider of PROVIDERS) {
|
|
if (provider.prefixes.some((prefix) => key.startsWith(prefix))) {
|
|
return { id: provider.id, label: provider.label, baseUrl: provider.baseUrl, model: provider.model, models: provider.models };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Maps a failed request to a message key the UI can translate. Every branch has
|
|
// to name something the user can actually do next — "401 Unauthorized" on its
|
|
// own tells a non-technical user nothing.
|
|
export function classifyFailure({ status, code } = {}) {
|
|
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'ECONNREFUSED') return 'key.err_offline';
|
|
if (code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT' || code === 'ABORT_ERR') return 'key.err_timeout';
|
|
if (status === 400 || status === 401 || status === 403) return 'key.err_rejected';
|
|
if (status === 429) return 'key.err_quota';
|
|
if (status === 404) return 'key.err_model';
|
|
if (typeof status === 'number' && status >= 500) return 'key.err_provider_down';
|
|
return 'key.err_unknown';
|
|
}
|
|
|
|
export function listProviders() {
|
|
return PROVIDERS.map(({ id, label, baseUrl, model, models }) => ({ id, label, baseUrl, model, models }));
|
|
}
|
|
|
|
// Google rotates model availability per account. When the first choice 404s, try the rest.
|
|
export function modelsToTry(provider, requestedModel) {
|
|
const primary = String(requestedModel || provider?.model || '').trim();
|
|
if (provider?.id === 'google') {
|
|
return [...new Set([primary, provider.model, ...(provider.models || [])].filter(Boolean))];
|
|
}
|
|
return primary ? [primary] : [];
|
|
}
|
|
|
|
// AQ auth keys only work on the OpenAI-compatible surface. Ask Google which models
|
|
// this key can actually see instead of guessing from a static list.
|
|
export async function discoverGoogleOpenAiModels(baseUrl, apiKey) {
|
|
const root = String(baseUrl || '').replace(/\/+$/, '');
|
|
if (!root || !apiKey) return [];
|
|
try {
|
|
const res = await fetch(`${root}/models`, {
|
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
});
|
|
if (!res.ok) return [];
|
|
const body = await res.json();
|
|
const rows = body.data || body.models || [];
|
|
return rows
|
|
.map((m) => String(m.id || m.name || '').replace(/^models\//, ''))
|
|
.filter(Boolean);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|