scrape / lib /llm.ts
kjbytes's picture
feat: replace custom LLM endpoint with Gemini 3.8 Flash
f7933f3
Raw History Blame Contribute Delete
3.27 kB
import {
enforceScriptLimit,
MAX_SCRIPT_CHARS,
MIN_SCRIPT_CHARS,
TARGET_SECONDS,
} from "@/lib/script-limits";
import type { GitHubRepoData } from "@/types";
// ponytail: 2.5 Flash is closed to new API keys; 3.8 Flash is Google's suggested replacement
const GEMINI_MODEL = "gemini-3.8-flash";
interface GeminiResponse {
candidates?: Array<{
content?: { parts?: Array<{ text?: string }> };
}>;
}
export async function callLlm(
prompt: string,
{ maxTokens = 300, temperature = 0.8, timeoutMs = 150_000 } = {},
): Promise<string> {
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) throw new Error("GEMINI_API_KEY is not set");
// ponytail: 3 tries with 2s/4s backoff on 429/503 ("high demand") — these are transient
let response!: Response;
for (let attempt = 1; attempt <= 3; attempt++) {
response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`,
{
method: "POST",
headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
body: JSON.stringify({
contents: [{ role: "user", parts: [{ text: prompt }] }],
generationConfig: {
temperature,
maxOutputTokens: maxTokens,
// ponytail: Flash thinks by default and burns maxOutputTokens on it; off for short copy
thinkingConfig: { thinkingBudget: 0 },
},
}),
signal: AbortSignal.timeout(timeoutMs),
},
);
if (response.status !== 429 && response.status !== 503) break;
if (attempt < 3) await new Promise((r) => setTimeout(r, 2000 * attempt));
}
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Gemini request failed (${response.status}): ${errorText.slice(0, 200)}`);
}
const data = (await response.json()) as GeminiResponse;
const raw = data.candidates?.[0]?.content?.parts
?.map((p) => p.text ?? "")
.join("")
.trim();
if (!raw) throw new Error("Gemini returned an empty response");
return raw;
}
export async function generateIntroScript(repo: GitHubRepoData): Promise<string> {
const prompt = `You write viral ${TARGET_SECONDS} second TikTok/Reels intro scripts for GitHub repos.
Repo: ${repo.fullName}
Description: ${repo.description || "No description"}
Language: ${repo.language}
Stars: ${repo.stars}
Topics: ${repo.topics.join(", ") || "none"}
README excerpt:
${repo.readme.slice(0, 2500)}
Write a punchy spoken intro script that:
- Takes ${TARGET_SECONDS} seconds to read aloud: 65-80 words, between ${MIN_SCRIPT_CHARS} and ${MAX_SCRIPT_CHARS} characters
- Never shorter than ${MIN_SCRIPT_CHARS} characters — add a concrete detail from the README rather than padding
- Opens with a hook about what the project does
- Highlights 2-3 key features or use cases
- Ends by asking viewers to comment below to get the repo link directly in their DM
- Uses short sentences, no markdown, no emojis
- Sounds natural when read aloud
Return ONLY the script text, nothing else.`;
const raw = await callLlm(prompt, { maxTokens: 300 });
const script = enforceScriptLimit(raw.replace(/^["']|["']$/g, ""));
if (!script) throw new Error("LLM returned empty script");
return script;
}