fix: honor Ollama Modelfile num_ctx discovery

This commit is contained in:
Peter Steinberger
2026-04-27 02:31:46 +01:00
parent 3f59cd0a09
commit 69daef8246
4 changed files with 88 additions and 2 deletions

View File

@@ -3,6 +3,7 @@ import { jsonResponse, requestBodyText, requestUrl } from "../../../src/test-hel
import {
buildOllamaModelDefinition,
enrichOllamaModelsWithContext,
parseOllamaNumCtxParameter,
resetOllamaModelShowInfoCacheForTest,
resolveOllamaApiBase,
type OllamaTagModel,
@@ -42,6 +43,58 @@ describe("ollama provider models", () => {
]);
});
it("uses Modelfile num_ctx when it expands the discovered context window", async () => {
const models: OllamaTagModel[] = [{ name: "llama3-32k:latest" }];
const fetchMock = vi.fn(async () =>
jsonResponse({
model_info: { "llama.context_length": 8192 },
parameters: 'stop "<|eot_id|>"\nnum_ctx 32768\nnum_keep 5',
capabilities: ["completion"],
}),
);
vi.stubGlobal("fetch", fetchMock);
const enriched = await enrichOllamaModelsWithContext("http://127.0.0.1:11434", models);
expect(enriched).toEqual([
{
name: "llama3-32k:latest",
contextWindow: 32768,
capabilities: ["completion"],
},
]);
});
it("keeps the larger native context window when Modelfile num_ctx is smaller", async () => {
const models: OllamaTagModel[] = [{ name: "llama3.2:latest" }];
const fetchMock = vi.fn(async () =>
jsonResponse({
model_info: { "llama.context_length": 131072 },
parameters: "num_ctx 4096",
}),
);
vi.stubGlobal("fetch", fetchMock);
const enriched = await enrichOllamaModelsWithContext("http://127.0.0.1:11434", models);
expect(enriched[0]?.contextWindow).toBe(131072);
});
it("uses positive num_ctx when /api/show omits model context metadata", async () => {
const models: OllamaTagModel[] = [{ name: "custom-model:latest" }];
const fetchMock = vi.fn(async () =>
jsonResponse({
model_info: {},
parameters: "num_ctx 16384",
}),
);
vi.stubGlobal("fetch", fetchMock);
const enriched = await enrichOllamaModelsWithContext("http://127.0.0.1:11434", models);
expect(enriched[0]?.contextWindow).toBe(16384);
});
it("sets models with vision capability from /api/show capabilities", async () => {
const models: OllamaTagModel[] = [{ name: "kimi-k2.5:cloud" }, { name: "glm-5.1:cloud" }];
const fetchMock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
@@ -225,4 +278,11 @@ describe("ollama provider models", () => {
expect(model.reasoning).toBe(false);
expect(model.compat?.supportsTools).toBe(false);
});
it("parses the last positive Modelfile num_ctx value", () => {
expect(parseOllamaNumCtxParameter("num_ctx 8192\nnum_ctx 32768")).toBe(32768);
expect(parseOllamaNumCtxParameter("temperature 0.8\nnum_ctx -1\nnum_ctx 0")).toBeUndefined();
expect(parseOllamaNumCtxParameter('stop "<|eot_id|>"')).toBeUndefined();
expect(parseOllamaNumCtxParameter({ num_ctx: 8192 })).toBeUndefined();
});
});

View File

@@ -95,6 +95,25 @@ function hasCachedOllamaModelShowInfo(info: OllamaModelShowInfo): boolean {
return typeof info.contextWindow === "number" || (info.capabilities?.length ?? 0) > 0;
}
export function parseOllamaNumCtxParameter(parameters: unknown): number | undefined {
if (typeof parameters !== "string" || !parameters.trim()) {
return undefined;
}
let lastValue: number | undefined;
for (const rawLine of parameters.split(/\r?\n/)) {
const match = rawLine.trim().match(/^num_ctx\s+(-?\d+)\b/);
if (!match) {
continue;
}
const parsed = Number.parseInt(match[1], 10);
if (Number.isFinite(parsed) && parsed > 0) {
lastValue = parsed;
}
}
return lastValue;
}
export async function queryOllamaModelShowInfo(
apiBase: string,
modelName: string,
@@ -119,6 +138,7 @@ export async function queryOllamaModelShowInfo(
const data = (await response.json()) as {
model_info?: Record<string, unknown>;
capabilities?: unknown;
parameters?: unknown;
};
let contextWindow: number | undefined;
@@ -138,6 +158,11 @@ export async function queryOllamaModelShowInfo(
}
}
const paramCtx = parseOllamaNumCtxParameter(data.parameters);
if (paramCtx !== undefined && (contextWindow === undefined || paramCtx > contextWindow)) {
contextWindow = paramCtx;
}
const capabilities = Array.isArray(data.capabilities)
? (data.capabilities as unknown[]).filter((c): c is string => typeof c === "string")
: undefined;