fix: never send an API key to the provider it wasn't entered for (v1.0.2)
All checks were successful
CI — Test & Build / Test & Build (push) Successful in 1m33s
All checks were successful
CI — Test & Build / Test & Build (push) Successful in 1m33s
A stored key carried no record of which provider it belonged to. Options
saves {provider, model} without the key whenever the field is blank (which
it always is after a save, since it shows the encrypted badge instead), so
switching provider left the previous provider's key attached to the new one.
Every call then failed with that provider's own "Invalid API Key" while the
UI still showed a key as configured.
- types.ts: new `keyProvider` storage field, added to CONFIG_STORAGE_KEYS
- background.ts: keyProviderMismatch() guards the chat, COPY_AS and
stored-key LIST_MODELS paths; absent keyProvider (pre-upgrade) is allowed
- Options.tsx: stamps keyProvider on every save; drops the encrypted badge
and requires a new key when the saved one belongs to another provider or
is rejected; save-time guard messages are now actually rendered (they were
gated on modelsStatus === 'error' and never drew, so Save looked dead)
- providers.ts: providerLabel(); settings hint appended to 401/403 only;
listModels reports keyRejected and labels errors with the display name
- Anthropic: send anthropic-dangerous-direct-browser-access on the chat path
Docs: CLAUDE.md version-bump rule corrected — wxt.config.ts reads
pkg.version, so package.json is the only place to edit.
typecheck clean, 58/58 tests, build clean (281.72 kB, manifest 1.0.2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -167,6 +167,9 @@ export const PROVIDER_SPECS: Record<string, ProviderSpec> = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
// Anthropic rejects browser-origin requests unless this opt-in is sent.
|
||||
// The service worker counts as a browser origin, so it's required here too.
|
||||
'anthropic-dangerous-direct-browser-access': 'true',
|
||||
}),
|
||||
body: (model, systemPrompt, text, maxTokens) => ({
|
||||
model,
|
||||
@@ -200,6 +203,16 @@ export const PROVIDER_SPECS: Record<string, ProviderSpec> = {
|
||||
},
|
||||
};
|
||||
|
||||
// Display name for a provider id ('groq' → 'Groq'), for user-facing messages.
|
||||
export function providerLabel(provider: string): string {
|
||||
return PROVIDER_SPECS[provider]?.label ?? provider;
|
||||
}
|
||||
|
||||
// A rejected key is the one failure users can actually fix, and the provider's
|
||||
// own wording ("Invalid API Key") doesn't say where to fix it.
|
||||
const KEY_HINT = ' — open LexAI Settings and re-enter your API key for this provider.';
|
||||
const isAuthStatus = (status: number) => status === 401 || status === 403;
|
||||
|
||||
// ─── Chat call ────────────────────────────────────────────────────────────────
|
||||
|
||||
// Scale the output budget with the input instead of the old hard-coded 1024
|
||||
@@ -244,7 +257,7 @@ export async function callProvider(
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) {
|
||||
const msg = data?.error?.message ?? `HTTP ${res.status}`;
|
||||
return { error: `${spec.label} error: ${msg}` };
|
||||
return { error: `${spec.label} error: ${msg}${isAuthStatus(res.status) ? KEY_HINT : ''}` };
|
||||
}
|
||||
|
||||
const result = spec.extract(data);
|
||||
@@ -260,7 +273,7 @@ const NON_CHAT_MODEL_RE = /embedding|whisper|tts|dall-e|audio|realtime|moderatio
|
||||
export async function listModels(
|
||||
provider: string,
|
||||
apiKey?: string,
|
||||
): Promise<{ models?: string[]; error?: string }> {
|
||||
): Promise<{ models?: string[]; error?: string; keyRejected?: boolean }> {
|
||||
const spec = PROVIDER_SPECS[provider];
|
||||
if (!spec) return { error: `Unknown provider: "${provider}". Please check LexAI settings.` };
|
||||
|
||||
@@ -280,13 +293,15 @@ export async function listModels(
|
||||
try {
|
||||
res = await fetchWithTimeout(spec.modelsUrl, { method: 'GET', headers }, 15000);
|
||||
} catch (err) {
|
||||
return { error: `Network error reaching ${provider}: ${String(err)}` };
|
||||
return { error: `Network error reaching ${spec.label}: ${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}` };
|
||||
// Flag rejected keys so Options can tell the user to re-enter one instead
|
||||
// of leaving the 🔒 badge implying the stored key is usable.
|
||||
return { error: `${spec.label} error: ${msg}`, ...(isAuthStatus(res.status) ? { keyRejected: true } : {}) };
|
||||
}
|
||||
|
||||
const raw = Array.isArray(data?.data) ? data.data : [];
|
||||
@@ -296,6 +311,6 @@ export async function listModels(
|
||||
.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}.` };
|
||||
if (ids.length === 0) return { error: `No models returned by ${spec.label}.` };
|
||||
return { models: ids };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user