Improve LLM client immutability and CI model defaults. (#9)
Release / generate-dungeon (push) Failing after 2m30s
Release / upload-to-gitea-release (push) Has been skipped

Replace mutable Ollama model export with a const fallback and initializeModel return value, resolving the model from the environment after optional API discovery. Use a for-of loop over attempt indices instead of let in the retry path.

Continue PDF generation when map image generation or upscaling fails, and avoid mutating request headers in place.

Document Open WebUI-style URLs in the README, pin OLLAMA_MODEL in the Gitea release workflow, and adjust integration and unit tests for the new initialization behavior.

Reviewed-on: #9
Co-authored-by: keligrubb <keligrubb324@gmail.com>
Co-committed-by: keligrubb <keligrubb324@gmail.com>
This commit was merged in pull request #9.
This commit is contained in:
2026-04-15 02:45:25 +00:00
committed by Keli Grubb
parent 4428cd4cb8
commit 7ea9f93dc8
7 changed files with 93 additions and 77 deletions
+17 -17
View File
@@ -2,7 +2,7 @@ import sharp from 'sharp';
import path from "path";
import { mkdir, writeFile } from "fs/promises";
import { fileURLToPath } from "url";
import { callOllama, OLLAMA_MODEL } from "./ollamaClient.js";
import { callOllama } from "./ollamaClient.js";
const COMFYUI_ENABLED = process.env.COMFYUI_ENABLED !== 'false';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -63,7 +63,7 @@ Input:
${flavor}
Output:`,
OLLAMA_MODEL,
undefined,
3,
"Generate Visual Prompt"
);
@@ -239,22 +239,22 @@ export async function generateDungeonImages({ flavor }) {
return path.join(__dirname, "dungeon_upscaled.png");
}
const finalPrompt = await generateVisualPrompt(flavor);
console.log("Engineered visual prompt:\n", finalPrompt);
try {
const finalPrompt = await generateVisualPrompt(flavor);
console.log("Engineered visual prompt:\n", finalPrompt);
const baseFilename = `dungeon.png`;
const upscaledFilename = `dungeon_upscaled.png`;
const baseFilename = `dungeon.png`;
const upscaledFilename = `dungeon_upscaled.png`;
const filepath = await generateImageViaComfyUI(finalPrompt, baseFilename);
if (!filepath) {
throw new Error("Failed to generate dungeon image.");
const filepath = await generateImageViaComfyUI(finalPrompt, baseFilename);
if (!filepath) return null;
const upscaledPath = await upscaleImage(filepath, upscaledFilename, 1456, 1024);
if (!upscaledPath) return null;
return upscaledPath;
} catch (err) {
console.warn("Map image failed:", err.message);
return null;
}
// Upscale 2x (half of A4 at 300dpi)
const upscaledPath = await upscaleImage(filepath, upscaledFilename, 1456, 1024);
if (!upscaledPath) {
throw new Error("Failed to upscale dungeon image.");
}
return upscaledPath;
}
+28 -14
View File
@@ -1,10 +1,16 @@
import { cleanText } from "./textUtils.js";
const OLLAMA_API_URL = process.env.OLLAMA_API_URL;
export let OLLAMA_MODEL = process.env.OLLAMA_MODEL || "gemma3:latest";
export const OLLAMA_MODEL = "qwen3.5-122b-a10b";
function effectiveModel(explicit) {
if (explicit !== undefined && explicit !== null) return explicit;
return process.env.OLLAMA_MODEL || OLLAMA_MODEL;
}
export async function initializeModel() {
if (process.env.OLLAMA_MODEL) return;
if (process.env.OLLAMA_MODEL) return process.env.OLLAMA_MODEL;
try {
const apiUrl = process.env.OLLAMA_API_URL;
const isOpenWebUI = apiUrl?.includes("/api/chat/completions");
@@ -16,17 +22,20 @@ export async function initializeModel() {
const res = await fetch(url, { headers });
if (res.ok) {
const data = await res.json();
const model = isOpenWebUI
const model = isOpenWebUI
? data.data?.[0]?.id || data.data?.[0]?.name
: data.models?.[0]?.name;
if (model) {
OLLAMA_MODEL = model;
process.env.OLLAMA_MODEL = model;
console.log(`Using default model: ${model}`);
return model;
}
}
} catch {
console.warn(`Could not fetch default model, using: ${OLLAMA_MODEL}`);
// fall through to warn below
}
console.warn(`Could not fetch default model, using: ${OLLAMA_MODEL}`);
return OLLAMA_MODEL;
}
export { cleanText };
@@ -45,8 +54,10 @@ async function sleep(ms) {
async function callOllamaBase(prompt, model, retries, stepName, apiType) {
const isUsingOpenWebUI = apiType === "open-webui";
const isUsingOllamaChat = apiType === "ollama-chat";
const resolvedModel = effectiveModel(model);
const attempts = Array.from({ length: retries }, (_, index) => index + 1);
for (let attempt = 1; attempt <= retries; attempt++) {
for (const attempt of attempts) {
try {
const promptCharCount = prompt.length;
const promptWordCount = prompt.split(/\s+/).length;
@@ -58,14 +69,17 @@ async function callOllamaBase(prompt, model, retries, stepName, apiType) {
`Prompt: ${promptCharCount} chars, ~${promptWordCount} words`,
);
const headers = { "Content-Type": "application/json" };
if (isUsingOpenWebUI && process.env.OLLAMA_API_KEY) {
headers["Authorization"] = `Bearer ${process.env.OLLAMA_API_KEY}`;
}
const headers =
isUsingOpenWebUI && process.env.OLLAMA_API_KEY
? {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.OLLAMA_API_KEY}`,
}
: { "Content-Type": "application/json" };
const body = isUsingOpenWebUI || isUsingOllamaChat
? { model, messages: [{ role: "user", content: prompt }] }
: { model, prompt, stream: false };
? { model: resolvedModel, messages: [{ role: "user", content: prompt }] }
: { model: resolvedModel, prompt, stream: false };
const response = await fetch(OLLAMA_API_URL, {
method: "POST",
@@ -108,7 +122,7 @@ async function callOllamaBase(prompt, model, retries, stepName, apiType) {
export async function callOllama(
prompt,
model = OLLAMA_MODEL,
model,
retries = 5,
stepName = "unknown",
) {
@@ -118,7 +132,7 @@ export async function callOllama(
export async function callOllamaExplicit(
prompt,
model = OLLAMA_MODEL,
model,
retries = 5,
stepName = "unknown",
apiType = "ollama-generate",