Files
homefeed/backend/src/inference/ollama-provider.ts
T
Claude ee585ea65c Fix silent Ollama prompt truncation in synthesis pipeline
Ollama was defaulting to a 4096-token context (vs. the model's 32768
training context) and silently truncating any oversized prompt by
dropping content from the middle, with no error surfaced anywhere —
observed losing ~53% of a merge-cluster prompt in production. Two
prompt builders (buildPrompt/buildRecapPrompt) concatenated all
source summaries/article bodies with no size cap, so a cluster with
enough sources (or a recap spanning enough articles) could easily
exceed the window.

Fix: OllamaProvider.generate() now always sends explicit num_ctx/
num_predict options (sized for CPU-only inference — i5-6600K, no GPU,
~17 tok/s prompt processing) instead of leaving Ollama to pick a
default. synthesis.ts now caps prompt size itself before it ever
reaches Ollama, giving each source/article an equal character budget
and trimming individual entries rather than dropping whole ones off
the end — every source stays at least partially represented and
attributable. Trims are logged via the existing admin log stream
instead of failing silently.
2026-07-27 03:31:31 +00:00

88 lines
3.3 KiB
TypeScript

import type { InferenceProvider } from './provider.js';
/**
* Default context window / max-generation length requested from Ollama when a caller
* doesn't specify its own. Ollama otherwise falls back to whatever the model's
* Modelfile/runner defaults to (observed as low as 4096 tokens for qwen2.5:7b-instruct
* here, well under that model's 32768-token training context) and SILENTLY truncates
* any prompt that doesn't fit — dropping the middle of the prompt with no error
* surfaced anywhere. Explicitly setting num_ctx/num_predict on every request makes the
* limit deliberate and stable instead of whatever Ollama happens to pick.
*
* 8192 is sized for CPU-only inference (the reference box is an i5-6600K running
* Ollama in Docker, no GPU, ~17 tokens/sec prompt processing) — RAM is not the
* constraint (48GB available; the KV cache for 8192 tokens is well under 1GB), but
* prompt-processing time scales with context, so this trades headroom against
* per-request latency rather than maxing out the model's full 32768-token capacity.
* Callers that build prompts (see pipeline/synthesis.ts) size their own content to fit
* within this budget up front, rather than relying on Ollama to truncate for them.
*/
export const DEFAULT_NUM_CTX = 8192;
export const DEFAULT_NUM_PREDICT = 700;
/**
* Talks to a self-hosted Ollama instance over HTTP. Address is a normal backend
* setting (GlobalSettings.aiServiceHost/Port), editable via the admin panel —
* see the Connections tab and "AI service connection" in the schema doc.
*/
export class OllamaProvider implements InferenceProvider {
constructor(
private host: string,
private port: number
) {}
private base(): string {
return `${this.host}:${this.port}`;
}
async generate(
prompt: string,
opts: { model?: string; system?: string; numCtx?: number; numPredict?: number } = {}
): Promise<string> {
const res = await fetch(`${this.base()}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: opts.model,
prompt,
system: opts.system,
stream: false,
options: {
num_ctx: opts.numCtx ?? DEFAULT_NUM_CTX,
num_predict: opts.numPredict ?? DEFAULT_NUM_PREDICT
}
})
});
if (!res.ok) throw new Error(`Ollama generate failed: ${res.status} ${await res.text()}`);
const data = (await res.json()) as { response: string };
return data.response;
}
async embed(text: string, opts: { model?: string } = {}): Promise<number[]> {
const res = await fetch(`${this.base()}/api/embeddings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: opts.model, prompt: text })
});
if (!res.ok) throw new Error(`Ollama embed failed: ${res.status} ${await res.text()}`);
const data = (await res.json()) as { embedding: number[] };
return data.embedding;
}
async listModels(): Promise<string[]> {
const res = await fetch(`${this.base()}/api/tags`);
if (!res.ok) throw new Error(`Ollama listModels failed: ${res.status}`);
const data = (await res.json()) as { models: { name: string }[] };
return data.models.map((m) => m.name);
}
async isReachable(): Promise<boolean> {
try {
const res = await fetch(`${this.base()}/api/tags`, { signal: AbortSignal.timeout(3000) });
return res.ok;
} catch {
return false;
}
}
}