import type { AppConfig } from "../config.js"; /** * Klient Groq (API zgodne z OpenAI chat/completions) — warstwa analizy * Maciusia. Nie generuje obrazów; służy do oceny wizyjnej i werdyktów. * Klucz w cfg.groqApiKey (z .env, nigdy w logach/odpowiedziach). */ export type GroqContentPart = | { type: "text"; text: string } | { type: "image_url"; image_url: { url: string } }; export interface GroqMessage { role: "system" | "user"; content: string | GroqContentPart[]; } export interface GroqChatOptions { messages: GroqMessage[]; /** wymusza odpowiedź JSON (tryb json_object) */ json?: boolean; maxTokens?: number; temperature?: number; model?: string; /** gpt-oss: "low" | "medium" | "high" — ogranicza rozumowanie, chroni budżet odpowiedzi */ reasoningEffort?: string; } export interface GroqChatResult { text: string; model: string; usage?: { prompt_tokens?: number; completion_tokens?: number } | undefined; } export function groqConfigured(cfg: AppConfig): boolean { return Boolean(cfg.groqApiKey); } export async function groqChat(cfg: AppConfig, opts: GroqChatOptions): Promise { if (!cfg.groqApiKey) throw new Error("Brak GROQ_API_KEY w konfiguracji (.env)."); const model = opts.model ?? cfg.groqModel; const res = await fetch(`${cfg.groqBaseUrl}/chat/completions`, { // twardy limit — bez niego zawieszone połączenie trzyma modal „Analizuję…” bez końca signal: AbortSignal.timeout(Number(process.env.VILMAL_GROQ_TIMEOUT_MS ?? 45_000)), method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.groqApiKey}`, }, body: JSON.stringify({ model, messages: opts.messages, temperature: opts.temperature ?? 0.2, max_completion_tokens: opts.maxTokens ?? 1024, ...(opts.json ? { response_format: { type: "json_object" } } : {}), ...(opts.reasoningEffort ? { reasoning_effort: opts.reasoningEffort } : {}), }), }); if (!res.ok) { const body = await res.text().catch(() => ""); throw new Error(`Groq HTTP ${res.status}: ${body.slice(0, 300)}`); } const data = (await res.json()) as { model?: string; choices?: Array<{ message?: { content?: string } }>; usage?: { prompt_tokens?: number; completion_tokens?: number }; }; const text = data.choices?.[0]?.message?.content; if (!text) throw new Error("Groq: pusta odpowiedź modelu."); return { text, model: data.model ?? model, usage: data.usage }; }