← back to Trademarks Copyright
src/lib/qwen.ts
59 lines
/**
* Local Qwen via Ollama at http://localhost:11434.
* Used for brand-name extraction and discovery suggestions in the unregistered-brand hunter.
*/
// P1 fix 2026-05-04: default to MS1 per `feedback_ollama_default_ms1.md`.
// Mac2 froze 2026-05-02 under GPU contention; trademarks-copyright pipelines
// (hunt + score + swot + drops) are bulk LLM workloads that should hit MS1.
const OLLAMA_URL = process.env.OLLAMA_URL || "http://192.168.1.133:11434";
const QWEN_MODEL = process.env.QWEN_MODEL || "qwen2.5:latest";
export async function qwen(
prompt: string,
opts: { temperature?: number; format?: "json"; model?: string } = {}
): Promise<string> {
const body: Record<string, unknown> = {
model: opts.model ?? QWEN_MODEL,
prompt,
stream: false,
options: { temperature: opts.temperature ?? 0.2 },
};
if (opts.format === "json") body.format = "json";
// P2 fix 2026-05-04: 120s timeout via AbortSignal. Without it a stalled
// Ollama hangs the route handler until Next.js maxDuration (300s) triggers.
const r = await fetch(`${OLLAMA_URL}/api/generate`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(120_000),
});
if (!r.ok) throw new Error(`Qwen HTTP ${r.status}: ${await r.text().catch(() => "")}`);
const j = (await r.json()) as { response: string };
return j.response.trim();
}
export async function qwenJSON<T = unknown>(prompt: string, temperature = 0.2): Promise<T> {
const raw = await qwen(prompt, { temperature, format: "json" });
try {
return JSON.parse(raw) as T;
} catch {
// Sometimes the model wraps JSON in fences — strip and retry.
const m = raw.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
if (m) return JSON.parse(m[0]) as T;
throw new Error(`Qwen JSON parse failed: ${raw.slice(0, 200)}`);
}
}
export async function qwenIsUp(): Promise<boolean> {
try {
const r = await fetch(`${OLLAMA_URL}/api/tags`);
return r.ok;
} catch {
return false;
}
}
export const QWEN_MODEL_NAME = QWEN_MODEL;