mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 01:24:31 +00:00
fix(ollama): unify context window handling across discovery, merge, and OpenAI-compat transport (#29205)
* fix(ollama): inject num_ctx for OpenAI-compatible transport * fix(ollama): discover per-model context and preserve higher limits * fix(agents): prefer matching provider model for fallback limits * fix(types): require numeric token limits in provider model merge * fix(types): accept unknown payload in ollama num_ctx wrapper * fix(types): simplify ollama settled-result extraction * config(models): add provider flag for Ollama OpenAI num_ctx injection * config(schema): allow provider num_ctx injection flag * config(labels): label provider num_ctx injection flag * config(help): document provider num_ctx injection flag * agents(ollama): gate OpenAI num_ctx injection with provider config * tests(ollama): cover provider num_ctx injection flag behavior * docs(config): list provider num_ctx injection option * docs(ollama): document OpenAI num_ctx injection toggle * docs(config): clarify merge token-limit precedence * config(help): note merge uses higher model token limits * fix(ollama): cap /api/show discovery concurrency * fix(ollama): restrict num_ctx injection to OpenAI compat * tests(ollama): cover ipv6 and compat num_ctx gating * fix(ollama): detect remote compat endpoints for ollama-labeled providers * fix(ollama): cap per-model /api/show lookups to bound discovery load
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveImplicitProviders, resolveOllamaApiBase } from "./models-config.providers.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("resolveOllamaApiBase", () => {
|
||||
it("returns default localhost base when no configured URL is provided", () => {
|
||||
expect(resolveOllamaApiBase()).toBe("http://127.0.0.1:11434");
|
||||
@@ -71,6 +76,110 @@ describe("Ollama provider", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("discovers per-model context windows from /api/show", async () => {
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
|
||||
process.env.OLLAMA_API_KEY = "test-key";
|
||||
vi.stubEnv("VITEST", "");
|
||||
vi.stubEnv("NODE_ENV", "development");
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
models: [
|
||||
{ name: "qwen3:32b", modified_at: "", size: 1, digest: "" },
|
||||
{ name: "llama3.3:70b", modified_at: "", size: 1, digest: "" },
|
||||
],
|
||||
}),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ model_info: { "qwen3.context_length": 131072 } }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ model_info: { "llama.context_length": 65536 } }),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
try {
|
||||
const providers = await resolveImplicitProviders({ agentDir });
|
||||
const models = providers?.ollama?.models ?? [];
|
||||
const qwen = models.find((model) => model.id === "qwen3:32b");
|
||||
const llama = models.find((model) => model.id === "llama3.3:70b");
|
||||
expect(qwen?.contextWindow).toBe(131072);
|
||||
expect(llama?.contextWindow).toBe(65536);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
} finally {
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to default context window when /api/show fails", async () => {
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
|
||||
process.env.OLLAMA_API_KEY = "test-key";
|
||||
vi.stubEnv("VITEST", "");
|
||||
vi.stubEnv("NODE_ENV", "development");
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
models: [{ name: "qwen3:32b", modified_at: "", size: 1, digest: "" }],
|
||||
}),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
try {
|
||||
const providers = await resolveImplicitProviders({ agentDir });
|
||||
const model = providers?.ollama?.models?.find((entry) => entry.id === "qwen3:32b");
|
||||
expect(model?.contextWindow).toBe(128000);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
}
|
||||
});
|
||||
|
||||
it("caps /api/show requests when /api/tags returns a very large model list", async () => {
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
|
||||
process.env.OLLAMA_API_KEY = "test-key";
|
||||
vi.stubEnv("VITEST", "");
|
||||
vi.stubEnv("NODE_ENV", "development");
|
||||
const manyModels = Array.from({ length: 250 }, (_, idx) => ({
|
||||
name: `model-${idx}`,
|
||||
modified_at: "",
|
||||
size: 1,
|
||||
digest: "",
|
||||
}));
|
||||
const fetchMock = vi.fn(async (url: string) => {
|
||||
if (url.endsWith("/api/tags")) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ models: manyModels }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ model_info: { "llama.context_length": 65536 } }),
|
||||
};
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
try {
|
||||
const providers = await resolveImplicitProviders({ agentDir });
|
||||
const models = providers?.ollama?.models ?? [];
|
||||
// 1 call for /api/tags + 200 capped /api/show calls.
|
||||
expect(fetchMock).toHaveBeenCalledTimes(201);
|
||||
expect(models).toHaveLength(200);
|
||||
} finally {
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
}
|
||||
});
|
||||
|
||||
it("should have correct model structure without streaming override", () => {
|
||||
const mockOllamaModel = {
|
||||
id: "llama3.3:latest",
|
||||
|
||||
Reference in New Issue
Block a user