feat: implement API key resolution and live model listing for providers
All checks were successful
CI — Test & Build / Test & Build (push) Successful in 59s
All checks were successful
CI — Test & Build / Test & Build (push) Successful in 59s
This commit is contained in:
@@ -80,6 +80,18 @@ async function decryptApiKey(encKeyB64: string, apiKeyEncB64: string): Promise<s
|
||||
return new TextDecoder().decode(decrypted);
|
||||
}
|
||||
|
||||
// Resolve the usable API key from stored config: prefer the encrypted path,
|
||||
// fall back to plaintext for backward compat. Returns null if none is set.
|
||||
async function resolveApiKey(config: LexAIConfig): Promise<string | null> {
|
||||
let apiKey = config.apiKey;
|
||||
if (config.apiKeyEnc && config.encKey) {
|
||||
const decrypted = await decryptApiKey(config.encKey, config.apiKeyEnc);
|
||||
if (decrypted) apiKey = decrypted;
|
||||
}
|
||||
if (!apiKey || apiKey.trim() === '') return null;
|
||||
return apiKey.trim();
|
||||
}
|
||||
|
||||
// ─── Provider implementations ─────────────────────────────────────────────────
|
||||
|
||||
async function callOpenAI(payload: AnalyzePayload, config: LexAIConfig): Promise<LexAIResponse> {
|
||||
@@ -353,26 +365,70 @@ async function callOpenRouterWithPrompt(payload: AnalyzePayload, config: LexAICo
|
||||
return { result: result.trim() };
|
||||
}
|
||||
|
||||
// ─── Live model listing ───────────────────────────────────────────────────────
|
||||
|
||||
const MODEL_LIST_ENDPOINTS: Record<string, string> = {
|
||||
openai: 'https://api.openai.com/v1/models',
|
||||
groq: 'https://api.groq.com/openai/v1/models',
|
||||
openrouter: 'https://openrouter.ai/api/v1/models',
|
||||
anthropic: 'https://api.anthropic.com/v1/models',
|
||||
};
|
||||
|
||||
// Drop non-chat models (embeddings, audio, image, etc.) so the picker stays useful.
|
||||
const NON_CHAT_MODEL_RE = /embedding|whisper|tts|dall-e|audio|realtime|moderation|image|guard|transcribe|speech|rerank/i;
|
||||
|
||||
async function listModels(provider: string, apiKey?: string): Promise<{ models?: string[]; error?: string }> {
|
||||
const url = MODEL_LIST_ENDPOINTS[provider];
|
||||
if (!url) return { error: `Unknown provider: "${provider}". Please check LexAI settings.` };
|
||||
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (provider === 'anthropic') {
|
||||
if (!apiKey) return { error: 'Anthropic requires an API key to list models.' };
|
||||
headers['x-api-key'] = apiKey;
|
||||
headers['anthropic-version'] = '2023-06-01';
|
||||
// Allow the extension origin to call Anthropic directly (avoids a CORS 403).
|
||||
headers['anthropic-dangerous-direct-browser-access'] = 'true';
|
||||
} else if (apiKey) {
|
||||
// OpenRouter's list is public, so the key is optional there; OpenAI/Groq require it.
|
||||
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetchWithTimeout(url, { method: 'GET', headers }, 15000);
|
||||
} catch (err) {
|
||||
return { error: `Network error reaching ${provider}: ${String(err)}` };
|
||||
}
|
||||
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) {
|
||||
const msg = data?.error?.message ?? data?.error ?? `HTTP ${res.status}`;
|
||||
return { error: `${provider} error: ${msg}` };
|
||||
}
|
||||
|
||||
const raw = Array.isArray(data?.data) ? data.data : [];
|
||||
const ids = raw
|
||||
.map((m: any) => (typeof m === 'string' ? m : m?.id))
|
||||
.filter((id: unknown): id is string => typeof id === 'string' && id.length > 0)
|
||||
.filter((id: string) => !NON_CHAT_MODEL_RE.test(id))
|
||||
.sort((a: string, b: string) => a.localeCompare(b));
|
||||
|
||||
if (ids.length === 0) return { error: `No models returned by ${provider}.` };
|
||||
return { models: ids };
|
||||
}
|
||||
|
||||
// ─── Main handler ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function handleAnalyzeText(payload: AnalyzePayload): Promise<LexAIResponse> {
|
||||
const stored = await chrome.storage.local.get(['provider', 'apiKey', 'apiKeyEnc', 'encKey', 'model']);
|
||||
const config = stored as LexAIConfig;
|
||||
|
||||
// Resolve API key — prefer encrypted path, fall back to plaintext for backward compat
|
||||
let apiKey = config.apiKey;
|
||||
if (config.apiKeyEnc && config.encKey) {
|
||||
const decrypted = await decryptApiKey(config.encKey, config.apiKeyEnc);
|
||||
if (decrypted) {
|
||||
apiKey = decrypted;
|
||||
}
|
||||
}
|
||||
|
||||
if (!apiKey || apiKey.trim() === '') {
|
||||
const apiKey = await resolveApiKey(config);
|
||||
if (!apiKey) {
|
||||
return { error: 'No API key configured. Please open LexAI settings (click the extension icon).' };
|
||||
}
|
||||
|
||||
const resolvedConfig: LexAIConfig = { ...config, apiKey: apiKey.trim() };
|
||||
const resolvedConfig: LexAIConfig = { ...config, apiKey };
|
||||
const provider = config.provider || 'openai';
|
||||
|
||||
switch (provider) {
|
||||
@@ -460,16 +516,12 @@ export default defineBackground(() => {
|
||||
chrome.storage.local.get(['provider', 'apiKey', 'apiKeyEnc', 'encKey', 'model'])
|
||||
.then(async (stored) => {
|
||||
const config = stored as LexAIConfig;
|
||||
let apiKey = config.apiKey;
|
||||
if (config.apiKeyEnc && config.encKey) {
|
||||
const decrypted = await decryptApiKey(config.encKey, config.apiKeyEnc);
|
||||
if (decrypted) apiKey = decrypted;
|
||||
}
|
||||
if (!apiKey || apiKey.trim() === '') {
|
||||
const apiKey = await resolveApiKey(config);
|
||||
if (!apiKey) {
|
||||
sendResponse({ error: 'No API key configured. Please open LexAI settings.' });
|
||||
return;
|
||||
}
|
||||
const resolvedConfig: LexAIConfig = { ...config, apiKey: apiKey.trim() };
|
||||
const resolvedConfig: LexAIConfig = { ...config, apiKey };
|
||||
const systemPrompt = `Reformat the following text as ${format}. Return only the reformatted result, no explanation.`;
|
||||
return callProvider(resolvedConfig, text, systemPrompt);
|
||||
})
|
||||
@@ -477,5 +529,25 @@ export default defineBackground(() => {
|
||||
.catch((err) => sendResponse({ error: String(err) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'LIST_MODELS') {
|
||||
const provider = (message.provider as string) || 'openai';
|
||||
// Prefer an inline key (freshly typed, not yet saved); else use the stored key.
|
||||
const inlineKey = (message.apiKey as string | undefined)?.trim() || undefined;
|
||||
(async () => {
|
||||
let apiKey = inlineKey;
|
||||
if (!apiKey) {
|
||||
const stored = await chrome.storage.local.get(['apiKey', 'apiKeyEnc', 'encKey']);
|
||||
apiKey = (await resolveApiKey(stored as LexAIConfig)) ?? undefined;
|
||||
}
|
||||
if (!apiKey && provider !== 'openrouter') {
|
||||
return { error: 'No API key found. Enter your API key above, then click Load models.' };
|
||||
}
|
||||
return listModels(provider, apiKey);
|
||||
})()
|
||||
.then(sendResponse)
|
||||
.catch((err) => sendResponse({ error: String(err) }));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user