Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/arkin26_why-join.json
T

1 line
14 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "arkin26/why-join", "files": {"extension/lib/llm.js": {"f0785d2b7fbac5852891412449716ccd3dca283d": {"t": 1785746707.122372, "n": 13281, "h": "11905bd708991f09", "c": "/**\n * Browser-safe multi-provider LLM client (fetch only — no Node SDKs).\n * Auto-detects from model name + API key shape.\n */\n\nconst GEMINI_API_ROOT = \"https://generativelanguage.googleapis.com/v1beta\";\n\nconst KNOWN_BASE_URLS = {\n groq: \"https://api.groq.com/openai/v1\",\n openrouter: \"https://openrouter.ai/api/v1\",\n deepseek: \"https://api.deepseek.com\",\n openai: \"https://api.openai.com/v1\",\n};\n\nexport function detectProviderAndBaseUrl({\n model,\n apiKey,\n provider,\n baseURL,\n} = {}) {\n const explicit = (provider || \"\").toLowerCase().trim();\n if (explicit && explicit !== \"auto\") {\n return {\n provider: explicit === \"google\" ? \"gemini\" : explicit,\n baseURL: baseURL || undefined,\n };\n }\n\n const m = String(model || \"\").toLowerCase().trim();\n const k = String(apiKey || \"\").trim();\n\n // Google AI Studio keys are usually AIza…; some newer keys use AQ.…\n if (k.startsWith(\"AIza\") || k.startsWith(\"AQ.\")) {\n return { provider: \"gemini\", baseURL: undefined };\n }\n if (k.startsWith(\"sk-ant-\")) {\n return { provider: \"anthropic\", baseURL: undefined };\n }\n if (k.startsWith(\"gsk_\")) {\n return { provider: \"compatible\", baseURL: baseURL || KNOWN_BASE_URLS.groq };\n }\n if (k.startsWith(\"sk-or-\")) {\n return {\n provider: \"compatible\",\n baseURL: baseURL || KNOWN_BASE_URLS.openrouter,\n };\n }\n\n if (m.startsWith(\"gemini\") || m.startsWith(\"gemma\") || m.includes(\"gemini-\")) {\n return { provider: \"gemini\", baseURL: undefined };\n }\n if (m.startsWith(\"claude\")) {\n return { provider: \"anthropic\", baseURL: undefined };\n }\n if (m.startsWith(\"deepseek\")) {\n return {\n provider: \"compatible\",\n baseURL: baseURL || KNOWN_BASE_URLS.deepseek,\n };\n }\n if (m.includes(\"/\")) {\n return {\n provider: \"compatible\",\n baseURL: baseURL || KNOWN_BASE_URLS.openrouter,\n };\n }\n if (\n m.startsWith(\"gpt-\") ||\n m.startsWith(\"chatgpt-\") ||\n m.startsWith(\"o1\") ||\n m.startsWith(\"o3\") ||\n m.startsWith(\"o4\")\n ) {\n return { provider: \"openai\", baseURL: undefined };\n }\n if (\n m.includes(\"llama\") ||\n m.includes(\"mixtral\") ||\n m.includes(\"gemma2\") ||\n m.includes(\"qwen\")\n ) {\n return { provider: \"compatible\", baseURL: baseURL || KNOWN_BASE_URLS.groq };\n }\n\n if (k.startsWith(\"sk-\")) {\n return { provider: \"openai\", baseURL: baseURL || undefined };\n }\n\n throw new Error(\n `Could not detect provider from model \"${model || \"(empty)\"}\". ` +\n `Try a clear name like gemini-3.6-flash, gpt-4o-mini, or claude-sonnet-4-20250514.`\n );\n}\n\nfunction normalizeGeminiModelId(model) {\n return String(model || \"\")\n .trim()\n .replace(/^models\\//, \"\");\n}\n\nfunction sleep(ms) {\n return new Promise((r) => setTimeout(r, ms));\n}\n\nexport function isRateLimitError(err) {\n const status = err?.status || err?.response?.status || err?.httpStatus;\n const body = err?.error?.message || err?.message || String(err);\n return (\n status === 429 ||\n /429|rate.?limit|quota|RESOURCE_EXHAUSTED|too many requests/i.test(body)\n );\n}\n\nasync function withRateLimitRetry(fn) {\n const delaysMs = [0, 8_000, 20_000];\n let lastErr;\n for (let i = 0; i < delaysMs.length; i++) {\n if (delaysMs[i] > 0) await sleep(delaysMs[i]);\n try {\n return await fn();\n } catch (err) {\n lastErr = err;\n if (!isRateLimitError(err) || i === delaysMs.length - 1) throw err;\n }\n }\n throw lastErr;\n}\n\n/** Never echo the raw API key in user-facing errors. */\nfunction redactSecrets(text, apiKey) {\n let out = String(text || \"\");\n if (apiKey && apiKey.length > 8) {\n out = out.split(apiKey).join(\"[redacted-key]\");\n }\n return out;\n}\n\nexport function formatLlmError(err, { provider, model, baseURL, apiKey }) {\n const status = err?.status || err?.response?.status || err?.httpStatus;\n const body = redactSecrets(\n err?.error?.message ||\n err?.message ||\n (typeof err === \"string\" ? err : String(err)),\n apiKey\n );\n\n const isOrFree =\n String(model || \"\").includes(\":free\") ||\n (baseURL || \"\").includes(\"openrouter.ai\");\n\n if (status === 404 || /404/.test(body)) {\n const hint =\n provider === \"gemini\"\n ? ` \"${model}\" isn’t available for this Google key. Try gemini-3.6-flash, gemini-3.5-flash, or gemini-flash-latest.`\n : ` Check that the model name matches your key’s provider.`;\n return new Error(`Model not found (404): ${body}.${hint}`);\n }\n\n if (isRateLimitError(err)) {\n const hint = isOrFree\n ? \" OpenRouter free tier is ~20/min and ~200/day. Wait, or use a Gemini / OpenAI key.\"\n : \" Wait 30–60 seconds and try again.\";\n return new Error(`Rate limit / quota (429): ${body}.${hint}`);\n }\n\n if (status === 401 || status === 403) {\n return new Error(\n `Auth failed (${status}): check your API key for ${provider}. ${body}`\n );\n }\n\n if (status === 400) {\n return new Error(`Bad request (400) [${provider}/${model}]: ${body}`);\n }\n\n return new Error(\n `LLM error${status ? ` ${status}` : \"\"} [${provider}/${model}]: ${body}`\n );\n}\n\nasync function geminiGenerateContent(apiKey, model, prompt, maxTokens) {\n const modelId = normalizeGeminiModelId(model);\n const url = `${GEMINI_API_ROOT}/models/${encodeURIComponent(modelId)}:generateContent`;\n const isGemini3 = /gemini-3/i.test(modelId);\n\n const generationConfig = {\n maxOutputTokens: isGemini3 ? Math.max(maxTokens, 1024) : maxTokens,\n };\n\n if (isGemini3) {\n generationConfig.thinkingConfig = { thinkingLevel: \"minimal\" };\n } else {\n generationConfig.temperature = 0.7;\n }\n\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-goog-api-key\": apiKey,\n },\n body: JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: prompt }] }],\n generationConfig,\n }),\n });\n\n const raw = await res.text();\n let data = {};\n try {\n data = raw ? JSON.parse(raw) : {};\n } catch {\n data = { raw };\n }\n\n if (!res.ok) {\n const msg =\n data?.error?.message ||\n (raw && raw.slice(0, 300)) ||\n `${res.status} ${res.statusText}`;\n const err = new Error(msg);\n err.httpStatus = res.status;\n throw err;\n }\n\n const parts = data?.candidates?.[0]?.content?.parts || [];\n const text = parts\n .filter((p) => !p.thought)\n .map((p) => p.text || \"\")\n .join(\"\")\n .trim();\n\n if (!text) {\n const block = data?.candidates?.[0]?.finishReason;\n throw new Error(\n `Gemini returned empty text${block ? ` (finishReason: ${block})` : \"\"}`\n );\n }\n\n return text;\n}\n\nasync function anthropicComplete(apiKey, model, prompt, maxTokens) {\n const res = await fetch(\"https://api.anthropic.com/v1/messages\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"anthropic-dangerous-direct-browser-access\": \"true\",\n },\n body: JSON.stringify({\n model,\n max_tokens: maxTokens,\n messages: [{ role: \"user\", content: prompt }],\n }),\n });\n\n const raw = await res.text();\n let data = {};\n try {\n data = raw ? JSON.parse(raw) : {};\n } catch {\n data = { raw };\n }\n\n if (!res.ok) {\n const msg =\n data?.error?.message ||\n (raw && raw.slice(0, 300)) ||\n `${res.status} ${res.statusText}`;\n const err = new Error(msg);\n err.httpStatus = res.status;\n throw err;\n }\n\n const textBlock = (data.content || []).find((b) => b.type === \"text\");\n if (!textBlock?.text) throw new Error(\"Anthropic returned no text content\");\n return textBlock.text.trim();\n}\n\nfunction extractChatText(completion) {\n const choice = completion?.choices?.[0];\n const message = choice?.message || {};\n\n if (typeof message.content === \"string\" && message.content.trim()) {\n return message.content.trim();\n }\n\n if (Array.isArray(message.content)) {\n const joined = message.content\n .map((part) => {\n if (typeof part === \"string\") return part;\n if (part?.text) return part.text;\n return \"\";\n })\n .join(\"\")\n .trim();\n if (joined) return joined;\n }\n\n if (typeof message.reasoning === \"string\" && message.reasoning.trim()) {\n return message.reasoning.trim();\n }\n if (\n typeof message.reasoning_content === \"string\" &&\n message.reasoning_content.trim()\n ) {\n return message.reasoning_content.trim();\n }\n\n return \"\";\n}\n\nfunction emptyCompletionError(completion) {\n const choice = completion?.choices?.[0];\n const finish = choice?.finish_reason;\n const nestedErr = choice?.error?.message || completion?.error?.message;\n const parts = [\n \"API returned no text\",\n finish ? `finish_reason=${finish}` : null,\n nestedErr ? `provider_error=${nestedErr}` : null,\n \"Retry once, or pick a different model.\",\n ].filter(Boolean);\n return new Error(parts.join(\". \"));\n}\n\nasync function openAiCompatibleComplete({\n apiKey,\n model,\n prompt,\n maxTokens,\n baseURL,\n}) {\n const root = (baseURL || KNOWN_BASE_URLS.openai).replace(/\\/$/, \"\");\n const url = `${root}/chat/completions`;\n const isOpenRouter = root.includes(\"openrouter.ai\");\n\n const headers = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n };\n if (isOpenRouter) {\n headers[\"HTTP-Referer\"] = \"https://github.com/wellfound-bot\";\n headers[\"X-Title\"] = \"Why Join Extension\";\n }\n\n const request = async () => {\n const res = await fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model,\n max_tokens: maxTokens,\n temperature: 0.7,\n messages: [{ role: \"user\", content: prompt }],\n }),\n });\n\n const raw = await res.text();\n let data = {};\n try {\n data = raw ? JSON.parse(raw) : {};\n } catch {\n data = { raw };\n }\n\n if (!res.ok) {\n const msg =\n data?.error?.message ||\n (raw && raw.slice(0, 300)) ||\n `${res.status} ${res.statusText}`;\n const err = new Error(msg);\n err.httpStatus = res.status;\n throw err;\n }\n return data;\n };\n\n let completion = await request();\n let text = extractChatText(completion);\n\n if (!text && isOpenRouter) {\n await sleep(1500);\n completion = await request();\n text = extractChatText(completion);\n }\n\n if (!text) throw emptyCompletionError(completion);\n return text;\n}\n\nfunction defaultModelFor(provider) {\n if (provider === \"gemini\") return \"gemini-3.6-flash\";\n if (provider === \"anthropic\") return \"claude-sonnet-4-20250514\";\n if (provider === \"openai\") return \"gpt-4o-mini\";\n return \"\";\n}\n\n/**\n * Guess provider label for UI without throwing.\n */\nexport function peekProvider({ apiKey, model } = {}) {\n try {\n return detectProviderAndBaseUrl({ apiKey, model, provider: \"auto\" })\n .provider;\n } catch {\n return \"auto\";\n }\n}\n\nexport function createLlmClient(overrides = {}) {\n const cleaned = Object.fromEntries(\n Object.entries(overrides).filter(\n ([, v]) => v !== undefined && v !== null && String(v).trim() !== \"\"\n )\n );\n\n const apiKey = String(cleaned.apiKey || \"\").trim();\n const modelInput = String(cleaned.model || \"\").trim();\n\n if (!apiKey) {\n throw new Error(\n \"Add your API key first (step 1). Keys stay on this device — never uploaded to us.\"\n );\n }\n if (!modelInput) {\n throw new Error(\n \"Enter a model name (step 2), e.g. gemini-3.6-flash, gpt-4o-mini, or claude-sonnet-4-20250514.\"\n );\n }\n\n const detected = detectProviderAndBaseUrl({\n apiKey,\n model: modelInput,\n provider: cleaned.provider || \"auto\",\n baseURL: cleaned.baseURL,\n });\n\n const cfg = {\n apiKey,\n model: modelInput || defaultModelFor(detected.provider),\n provider: detected.provider,\n baseURL: cleaned.baseURL || detected.baseURL,\n };\n\n if (cfg.provider === \"gemini\") {\n const model = normalizeGeminiModelId(cfg.model);\n return {\n provider: \"gemini\",\n model,\n async complete(prompt, { maxTokens = 500 } = {}) {\n try {\n return await withRateLimitRetry(() =>\n geminiGenerateContent(cfg.apiKey, model, prompt, maxTokens)\n );\n } catch (err) {\n throw formatLlmError(err, { ...cfg, model });\n }\n },\n };\n }\n\n if (cfg.provider === \"anthropic\") {\n return {\n provider: \"anthropic\",\n model: cfg.model,\n async complete(prompt, { maxTokens = 500 } = {}) {\n try {\n return await withRateLimitRetry(() =>\n anthropicComplete(cfg.apiKey, cfg.model, prompt, maxTokens)\n );\n } catch (err) {\n throw formatLlmError(err, cfg);\n }\n },\n };\n }\n\n if (cfg.provider === \"openai\" || cfg.provider === \"compatible\") {\n return {\n provider: cfg.provider,\n model: cfg.model,\n async complete(prompt, { maxTokens = 800 } = {}) {\n try {\n return await withRateLimitRetry(() =>\n openAiCompatibleComplete({\n apiKey: cfg.apiKey,\n model: cfg.model,\n prompt,\n maxTokens,\n baseURL: cfg.baseURL,\n })\n );\n } catch (err) {\n throw formatLlmError(err, cfg);\n }\n },\n };\n }\n\n throw new Error(`Unknown provider \"${cfg.provider}\" after auto-detect.`);\n}\n"}}}}