import { enforceScriptLimit, MAX_SCRIPT_CHARS, MIN_SCRIPT_CHARS, TARGET_SECONDS, } from "@/lib/script-limits"; import type { GitHubRepoData } from "@/types"; const DEFAULT_LLM_URL = "https://cocoxkhushal.online/v1/chat/completions"; interface ChatCompletionResponse { choices?: Array<{ message?: { content?: string; }; }>; } function getLlmUrl(): string { return process.env.LLM_API_URL || DEFAULT_LLM_URL; } function buildHeaders(): Record { const headers: Record = { accept: "application/json", "Content-Type": "application/json", }; const apiKey = process.env.LLM_API_KEY || process.env.OPENAI_API_KEY; if (apiKey) { headers.Authorization = `Bearer ${apiKey}`; } return headers; } // ponytail: one shared call so social copy doesn't duplicate the fetch/parse dance export async function callLlm( prompt: string, { maxTokens = 300, temperature = 0.8, timeoutMs = 150_000 } = {}, ): Promise { const body: Record = { messages: [{ role: "user", content: prompt }], temperature, top_p: 1, max_tokens: maxTokens, stream: false, top_k: 0, presence_penalty: 0, frequency_penalty: 0, }; const model = process.env.LLM_MODEL || process.env.OPENAI_MODEL; if (model) body.model = model; // ponytail: this model occasionally stalls; bail at timeoutMs so callers can retry // instead of hanging on undici's 300s headers timeout const response = await fetch(getLlmUrl(), { method: "POST", headers: buildHeaders(), body: JSON.stringify(body), signal: AbortSignal.timeout(timeoutMs), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`LLM request failed (${response.status}): ${errorText.slice(0, 200)}`); } const data = (await response.json()) as ChatCompletionResponse; const raw = data.choices?.[0]?.message?.content?.trim(); if (!raw) throw new Error("LLM returned an empty response"); return raw; } export async function generateIntroScript(repo: GitHubRepoData): Promise { 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 with a call to check the repo - 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; }