mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 22:12:32 +08:00
fix(ollama): resolve cloud models omitted from tags
This commit is contained in:
@@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Model selection: include the rejected provider/model ref and allowlist recovery hint when a stored session override is cleared, so local model selections such as Gemma GGUF variants do not fall back to the default with a generic message. Refs #71069. Thanks @CyberRaccoonTeam.
|
||||
- OpenAI-compatible providers: drop malformed event-only or blank-data SSE frames before the OpenAI SDK stream parser sees them, so proxies that split `event:` from `data:` no longer crash streaming runs with `Unexpected end of JSON input`. Fixes #52802. Thanks @LyHug.
|
||||
- Gateway/OpenAI-compatible streaming: strip `<final>` tags split across streamed model deltas before they reach SSE clients, so `/v1/chat/completions` no longer emits tag remnants or drops content when final-answer wrappers cross chunk boundaries. Fixes #63325. Thanks @tzwickl.
|
||||
- Ollama: resolve explicitly selected signed-in `:cloud` models through `/api/show` when `/api/tags` omits them, so working models such as `gemini-3-flash-preview:cloud` and `deepseek-v4-pro:cloud` do not fail dynamic model resolution before the native `/api/chat` transport runs. Fixes #73909. Thanks @chtse53.
|
||||
- Local model prompt caching: keep stable Project Context above volatile channel/session prompt guidance and stop embedding current channel names in the message tool description, so Ollama, MLX, llama.cpp, and other prefix-cache backends avoid avoidable full prompt reprocessing across channel turns. Fixes #40256; supersedes #40296. Thanks @rhclaw and @sriram369.
|
||||
- Gateway/OpenAI-compatible API: guard provider policy lookup against runtime providers with non-array `models` values, so `/v1/chat/completions` no longer fails with `provider?.models?.some is not a function`. Fixes #66744; carries forward #66761. Thanks @MightyMoud, @MukundaKatta.
|
||||
- WhatsApp/Web: pass explicit Baileys socket timings into every WhatsApp Web socket and expose `web.whatsapp.*` keepalive, connect, and query timeout settings so unstable networks can avoid repeated 408 disconnect and opening-handshake timeout loops. Fixes #56365. (#73580) Thanks @velvet-shark.
|
||||
|
||||
@@ -192,6 +192,12 @@ When you set `OLLAMA_API_KEY` (or an auth profile) and **do not** define `models
|
||||
|
||||
This avoids manual model entries while keeping the catalog aligned with the local Ollama instance. You can use a full ref such as `ollama/<pulled-model>:latest` in local `infer model run`; OpenClaw resolves that installed model from Ollama's live catalog without requiring a hand-written `models.json` entry.
|
||||
|
||||
For signed-in Ollama hosts, some `:cloud` models may be usable through `/api/chat`
|
||||
and `/api/show` before they appear in `/api/tags`. When you explicitly select a
|
||||
full `ollama/<model>:cloud` ref, OpenClaw validates that exact missing model with
|
||||
`/api/show` and adds it to the runtime catalog only if Ollama confirms model
|
||||
metadata. Typos still fail as unknown models instead of being auto-created.
|
||||
|
||||
```bash
|
||||
# See what models are available
|
||||
ollama list
|
||||
|
||||
@@ -20,6 +20,21 @@ const promptAndConfigureOllamaMock = vi.hoisted(() =>
|
||||
);
|
||||
const ensureOllamaModelPulledMock = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const buildOllamaProviderMock = vi.hoisted(() => vi.fn());
|
||||
const queryOllamaModelShowInfoMock = vi.hoisted(() => vi.fn());
|
||||
const buildOllamaModelDefinitionMock = vi.hoisted(() =>
|
||||
vi.fn((modelId: string, contextWindow?: number, capabilities?: string[]) => ({
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
reasoning: capabilities?.includes("thinking") ?? false,
|
||||
input: capabilities?.includes("vision") ? ["text", "image"] : ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: contextWindow ?? 8192,
|
||||
maxTokens: 8192,
|
||||
compat: capabilities
|
||||
? { supportsTools: capabilities.includes("tools"), supportsUsageInStreaming: true }
|
||||
: { supportsUsageInStreaming: true },
|
||||
})),
|
||||
);
|
||||
const createConfiguredOllamaStreamFnMock = vi.hoisted(() =>
|
||||
vi.fn((_params: { model: unknown; providerBaseUrl?: string }) => ({}) as never),
|
||||
);
|
||||
@@ -29,6 +44,8 @@ vi.mock("./api.js", () => ({
|
||||
ensureOllamaModelPulled: ensureOllamaModelPulledMock,
|
||||
configureOllamaNonInteractive: vi.fn(),
|
||||
buildOllamaProvider: buildOllamaProviderMock,
|
||||
queryOllamaModelShowInfo: queryOllamaModelShowInfoMock,
|
||||
buildOllamaModelDefinition: buildOllamaModelDefinitionMock,
|
||||
}));
|
||||
|
||||
vi.mock("./src/stream.js", async (importOriginal) => {
|
||||
@@ -43,6 +60,8 @@ beforeEach(() => {
|
||||
promptAndConfigureOllamaMock.mockClear();
|
||||
ensureOllamaModelPulledMock.mockClear();
|
||||
buildOllamaProviderMock.mockReset();
|
||||
queryOllamaModelShowInfoMock.mockReset();
|
||||
buildOllamaModelDefinitionMock.mockClear();
|
||||
createConfiguredOllamaStreamFnMock.mockClear();
|
||||
});
|
||||
|
||||
@@ -420,6 +439,102 @@ describe("ollama plugin", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves requested Ollama cloud models that are omitted from tags but confirmed by show", async () => {
|
||||
const provider = registerProvider();
|
||||
const previous = process.env.OLLAMA_API_KEY;
|
||||
process.env.OLLAMA_API_KEY = "ollama-local";
|
||||
buildOllamaProviderMock.mockResolvedValueOnce({
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
api: "ollama",
|
||||
models: [
|
||||
{
|
||||
id: "kimi-k2.5:cloud",
|
||||
name: "kimi-k2.5:cloud",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
],
|
||||
});
|
||||
queryOllamaModelShowInfoMock.mockResolvedValueOnce({
|
||||
contextWindow: 1048576,
|
||||
capabilities: ["completion", "tools", "thinking"],
|
||||
});
|
||||
|
||||
try {
|
||||
await provider.prepareDynamicModel?.({
|
||||
config: {},
|
||||
provider: "ollama",
|
||||
modelId: "deepseek-v4-pro:cloud",
|
||||
modelRegistry: { find: vi.fn(() => null) },
|
||||
} as never);
|
||||
|
||||
expect(queryOllamaModelShowInfoMock).toHaveBeenCalledWith(
|
||||
"http://127.0.0.1:11434",
|
||||
"deepseek-v4-pro:cloud",
|
||||
);
|
||||
expect(
|
||||
provider.resolveDynamicModel?.({
|
||||
config: {},
|
||||
provider: "ollama",
|
||||
modelId: "deepseek-v4-pro:cloud",
|
||||
modelRegistry: { find: vi.fn(() => null) },
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
provider: "ollama",
|
||||
id: "deepseek-v4-pro:cloud",
|
||||
api: "ollama",
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
reasoning: true,
|
||||
compat: { supportsTools: true },
|
||||
});
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
} else {
|
||||
process.env.OLLAMA_API_KEY = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps unknown requested Ollama models unresolved when show has no metadata", async () => {
|
||||
const provider = registerProvider();
|
||||
const previous = process.env.OLLAMA_API_KEY;
|
||||
process.env.OLLAMA_API_KEY = "ollama-local";
|
||||
buildOllamaProviderMock.mockResolvedValueOnce({
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
api: "ollama",
|
||||
models: [],
|
||||
});
|
||||
queryOllamaModelShowInfoMock.mockResolvedValueOnce({});
|
||||
|
||||
try {
|
||||
await provider.prepareDynamicModel?.({
|
||||
config: {},
|
||||
provider: "ollama",
|
||||
modelId: "depseek-v4-pro:cloud",
|
||||
modelRegistry: { find: vi.fn(() => null) },
|
||||
} as never);
|
||||
|
||||
expect(
|
||||
provider.resolveDynamicModel?.({
|
||||
config: {},
|
||||
provider: "ollama",
|
||||
modelId: "depseek-v4-pro:cloud",
|
||||
modelRegistry: { find: vi.fn(() => null) },
|
||||
} as never),
|
||||
).toBeUndefined();
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
} else {
|
||||
process.env.OLLAMA_API_KEY = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("skips implicit localhost discovery when a custom remote Ollama provider is configured", async () => {
|
||||
const provider = registerProvider();
|
||||
|
||||
|
||||
@@ -19,10 +19,13 @@ import {
|
||||
OPENAI_COMPATIBLE_REPLAY_HOOKS,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
OLLAMA_DEFAULT_BASE_URL,
|
||||
buildOllamaModelDefinition,
|
||||
buildOllamaProvider,
|
||||
configureOllamaNonInteractive,
|
||||
ensureOllamaModelPulled,
|
||||
promptAndConfigureOllama,
|
||||
queryOllamaModelShowInfo,
|
||||
} from "./api.js";
|
||||
import {
|
||||
OLLAMA_DEFAULT_API_KEY,
|
||||
@@ -100,6 +103,29 @@ function toDynamicOllamaModel(params: {
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveRequestedDynamicOllamaModel(params: {
|
||||
provider: string;
|
||||
providerConfig: ModelProviderConfig;
|
||||
modelId: string;
|
||||
}): Promise<ProviderRuntimeModel | undefined> {
|
||||
const showInfo = await queryOllamaModelShowInfo(
|
||||
readProviderBaseUrl(params.providerConfig) ?? OLLAMA_DEFAULT_BASE_URL,
|
||||
params.modelId,
|
||||
);
|
||||
if (typeof showInfo.contextWindow !== "number" && (showInfo.capabilities?.length ?? 0) === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return toDynamicOllamaModel({
|
||||
provider: params.provider,
|
||||
providerConfig: params.providerConfig,
|
||||
model: buildOllamaModelDefinition(
|
||||
params.modelId,
|
||||
showInfo.contextWindow,
|
||||
showInfo.capabilities,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "ollama",
|
||||
name: "Ollama Provider",
|
||||
@@ -268,16 +294,24 @@ export default definePluginEntry({
|
||||
}
|
||||
const baseUrl = readProviderBaseUrl(providerConfig);
|
||||
const provider = await buildOllamaProvider(baseUrl, { quiet: true });
|
||||
dynamicModelCache.set(
|
||||
buildDynamicCacheKey(ctx.provider, baseUrl),
|
||||
(provider.models ?? []).map((model) =>
|
||||
toDynamicOllamaModel({
|
||||
provider: ctx.provider,
|
||||
providerConfig: provider,
|
||||
model,
|
||||
}),
|
||||
),
|
||||
const dynamicModels = (provider.models ?? []).map((model) =>
|
||||
toDynamicOllamaModel({
|
||||
provider: ctx.provider,
|
||||
providerConfig: provider,
|
||||
model,
|
||||
}),
|
||||
);
|
||||
if (!dynamicModels.some((model) => model.id === ctx.modelId)) {
|
||||
const requestedModel = await resolveRequestedDynamicOllamaModel({
|
||||
provider: ctx.provider,
|
||||
providerConfig: provider,
|
||||
modelId: ctx.modelId,
|
||||
});
|
||||
if (requestedModel) {
|
||||
dynamicModels.push(requestedModel);
|
||||
}
|
||||
}
|
||||
dynamicModelCache.set(buildDynamicCacheKey(ctx.provider, baseUrl), dynamicModels);
|
||||
},
|
||||
resolveDynamicModel: (ctx) => {
|
||||
const providerConfig = resolveConfiguredOllamaProviderConfig({
|
||||
|
||||
Reference in New Issue
Block a user