refactor: keep ollama compat in extension

This commit is contained in:
Peter Steinberger
2026-04-18 19:42:10 +01:00
parent d3eeadba94
commit 5dbfaa15fa
4 changed files with 46 additions and 53 deletions

View File

@@ -10,6 +10,7 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
import {
buildOllamaChatRequest,
createConfiguredOllamaCompatStreamWrapper,
createConfiguredOllamaStreamFn,
createOllamaStreamFn,
convertToOllamaMessages,
@@ -57,6 +58,46 @@ describe("buildOllamaChatRequest", () => {
});
});
describe("createConfiguredOllamaCompatStreamWrapper", () => {
it("adds Moonshot thinking config for Ollama cloud Kimi compat requests", async () => {
let patchedPayload: Record<string, unknown> | undefined;
const baseStreamFn = vi.fn((_model, _context, options) => {
options?.onPayload?.({ tool_choice: "auto" });
return (async function* () {})();
});
const model = {
api: "openai-completions",
provider: "ollama",
id: "kimi-k2.5:cloud",
contextWindow: 262144,
};
const wrapped = createConfiguredOllamaCompatStreamWrapper({
provider: "ollama",
modelId: "kimi-k2.5:cloud",
model,
streamFn: baseStreamFn,
thinkingLevel: "high",
extraParams: {},
} as never);
await wrapped?.(
model as never,
{ messages: [] } as never,
{
onPayload: (payload: unknown) => {
patchedPayload = payload as Record<string, unknown>;
},
} as never,
);
expect(patchedPayload).toMatchObject({
thinking: { type: "enabled" },
options: { num_ctx: 262144 },
});
});
});
describe("convertToOllamaMessages", () => {
it("converts user text messages", () => {
const messages = [{ role: "user", content: "hello" }];

View File

@@ -67,7 +67,6 @@ export function resolveOpenAICompletionsCompatDefaults(
endpointClass === "mistral-public" ||
knownProviderFamily === "mistral" ||
(isDefaultRoute && isDefaultRouteProvider(provider, "chutes"));
const isOllamaCompatProvider = provider === "ollama";
return {
supportsStore:
!isNonStandard && knownProviderFamily !== "mistral" && !usesExplicitProxyLikeEndpoint,
@@ -78,8 +77,7 @@ export function resolveOpenAICompletionsCompatDefaults(
endpointClass !== "xai-native" &&
!usesExplicitProxyLikeEndpoint,
supportsUsageInStreaming:
isOllamaCompatProvider ||
(!isNonStandard && (!usesConfiguredNonOpenAIEndpoint || supportsNativeStreamingUsageCompat)),
!isNonStandard && (!usesConfiguredNonOpenAIEndpoint || supportsNativeStreamingUsageCompat),
maxTokensField: usesMaxTokens ? "max_tokens" : "max_completion_tokens",
thinkingFormat: isZai ? "zai" : isOpenRouterLike ? "openrouter" : "openai",
visibleReasoningDetailTypes: isOpenRouterLike ? ["response.output_text", "response.text"] : [],

View File

@@ -712,7 +712,7 @@ describe("provider attribution", () => {
expect(
resolveProviderRequestCapabilities({
provider: "ollama",
provider: "custom-local",
api: "openai-completions",
baseUrl: "http://127.0.0.1:11434/v1",
capability: "llm",
@@ -720,20 +720,7 @@ describe("provider attribution", () => {
}),
).toMatchObject({
endpointClass: "local",
supportsNativeStreamingUsageCompat: true,
});
expect(
resolveProviderRequestCapabilities({
provider: "ollama",
api: "openai-completions",
baseUrl: "http://127.0.0.1:11434/v1",
capability: "llm",
transport: "stream",
modelId: "kimi-k2.5:cloud",
}),
).toMatchObject({
compatibilityFamily: "moonshot",
supportsNativeStreamingUsageCompat: false,
});
});
@@ -928,28 +915,6 @@ describe("provider attribution", () => {
supportsNativeStreamingUsageCompat: true,
},
},
{
name: "Ollama OpenAI-compatible completions",
input: {
provider: "ollama",
api: "openai-completions",
baseUrl: "http://127.0.0.1:11434/v1",
capability: "llm" as const,
transport: "stream" as const,
},
expected: {
knownProviderFamily: "ollama",
endpointClass: "local",
isKnownNativeEndpoint: false,
allowsOpenAIServiceTier: false,
supportsOpenAIReasoningCompatPayload: false,
allowsResponsesStore: false,
supportsResponsesStoreField: false,
shouldStripResponsesPromptCache: false,
allowsAnthropicServiceTier: false,
supportsNativeStreamingUsageCompat: true,
},
},
{
name: "native Google Gemini api",
input: {

View File

@@ -132,10 +132,6 @@ const OPENAI_RESPONSES_APIS = new Set([
const OPENAI_RESPONSES_PROVIDERS = new Set(["openai", "azure-openai", "azure-openai-responses"]);
const MOONSHOT_COMPAT_PROVIDERS = new Set(["moonshot", "kimi"]);
function isOllamaMoonshotCompatModel(modelId: string | null | undefined): boolean {
return /^kimi-k2\.5(?::|$)/i.test(modelId?.trim() ?? "");
}
function formatOpenClawUserAgent(version: string): string {
return `${OPENCLAW_ATTRIBUTION_ORIGINATOR}/${version}`;
}
@@ -575,12 +571,7 @@ export function resolveProviderRequestCapabilities(
endpointClass === "google-vertex";
let compatibilityFamily: ProviderRequestCompatibilityFamily | undefined;
const isOllamaOpenAICompletions = provider === "ollama" && api === "openai-completions";
if (
provider &&
(MOONSHOT_COMPAT_PROVIDERS.has(provider) ||
(provider === "ollama" && isOllamaMoonshotCompatModel(input.modelId)))
) {
if (provider && MOONSHOT_COMPAT_PROVIDERS.has(provider)) {
compatibilityFamily = "moonshot";
}
@@ -638,9 +629,7 @@ export function resolveProviderRequestCapabilities(
// Native endpoint class is the real signal here. Users can point a generic
// provider key at Moonshot or DashScope and still need streaming usage.
supportsNativeStreamingUsageCompat:
endpointClass === "moonshot-native" ||
endpointClass === "modelstudio-native" ||
isOllamaOpenAICompletions,
endpointClass === "moonshot-native" || endpointClass === "modelstudio-native",
compatibilityFamily,
};
}