mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-23 07:38:10 +00:00
fix(ollama): preserve configured API during discovery (#93729)
* fix(ollama): preserve configured API during discovery * fix(ollama): keep compatible discovery base URL * fix(ollama): route compatible APIs through configured transport --------- Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
This commit is contained in:
@@ -467,6 +467,68 @@ describe("ollama plugin", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves explicit api for configured dynamic Ollama models", async () => {
|
||||
const provider = registerProvider();
|
||||
const previous = process.env.OLLAMA_API_KEY;
|
||||
process.env.OLLAMA_API_KEY = "ollama-live";
|
||||
buildOllamaProviderMock.mockResolvedValueOnce({
|
||||
baseUrl: "https://ollama.example.com",
|
||||
api: "ollama",
|
||||
models: [
|
||||
{
|
||||
id: "qwen3-coder:cloud",
|
||||
name: "qwen3-coder:cloud",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 8192,
|
||||
maxTokens: 2048,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
const config = {
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
baseUrl: "https://ollama.example.com/v1",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await provider.prepareDynamicModel?.({
|
||||
config,
|
||||
provider: "ollama",
|
||||
modelId: "qwen3-coder:cloud",
|
||||
modelRegistry: { find: vi.fn(() => null) },
|
||||
} as never);
|
||||
|
||||
const resolved = provider.resolveDynamicModel?.({
|
||||
config,
|
||||
provider: "ollama",
|
||||
modelId: "qwen3-coder:cloud",
|
||||
modelRegistry: { find: vi.fn(() => null) },
|
||||
} as never);
|
||||
expect(resolved?.provider).toBe("ollama");
|
||||
expect(resolved?.id).toBe("qwen3-coder:cloud");
|
||||
expect(resolved?.api).toBe("openai-completions");
|
||||
expect(resolved?.baseUrl).toBe("https://ollama.example.com/v1");
|
||||
expect(buildOllamaProviderMock).toHaveBeenCalledWith("https://ollama.example.com/v1", {
|
||||
quiet: true,
|
||||
});
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
} else {
|
||||
process.env.OLLAMA_API_KEY = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -1335,7 +1397,7 @@ describe("ollama plugin", () => {
|
||||
|
||||
provider.createStreamFn?.({
|
||||
config: {},
|
||||
model: { id: "kimi-k2.5:cloud" },
|
||||
model: { api: "ollama", id: "kimi-k2.5:cloud" },
|
||||
provider: "ollama-cloud",
|
||||
} as never);
|
||||
expect(requireConfiguredStreamParams().providerBaseUrl).toBe("https://ollama.com");
|
||||
@@ -1434,6 +1496,55 @@ describe("ollama plugin", () => {
|
||||
expect(nativePolicy?.validateAnthropicTurns).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
providerId: "ollama",
|
||||
register: registerProvider,
|
||||
nativeBaseUrl: "http://127.0.0.1:11434",
|
||||
},
|
||||
{
|
||||
providerId: "ollama-cloud",
|
||||
register: registerOllamaCloudProvider,
|
||||
nativeBaseUrl: "https://ollama.com",
|
||||
},
|
||||
])(
|
||||
"$providerId selects native /api/chat transport only for api=ollama",
|
||||
({ providerId, register, nativeBaseUrl }) => {
|
||||
const provider = register();
|
||||
const createStream = (api: "ollama" | "openai-completions", baseUrl: string) =>
|
||||
provider.createStreamFn?.({
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
[providerId]: {
|
||||
api,
|
||||
baseUrl,
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
model: {
|
||||
api,
|
||||
id: "qwen3:32b",
|
||||
provider: providerId,
|
||||
},
|
||||
provider: providerId,
|
||||
} as never);
|
||||
|
||||
const compatibleStream = createStream("openai-completions", `${nativeBaseUrl}/v1`);
|
||||
|
||||
expect(compatibleStream).toBeUndefined();
|
||||
expect(createConfiguredOllamaStreamFnMock).not.toHaveBeenCalled();
|
||||
|
||||
const nativeStream = createStream("ollama", nativeBaseUrl);
|
||||
|
||||
expect(nativeStream).toBeDefined();
|
||||
expect(createConfiguredOllamaStreamFnMock).toHaveBeenCalledOnce();
|
||||
expect(requireConfiguredStreamParams().providerBaseUrl).toBe(nativeBaseUrl);
|
||||
},
|
||||
);
|
||||
|
||||
it("routes createStreamFn to the correct provider baseUrl for ollama2", () => {
|
||||
const provider = registerProvider();
|
||||
const config = {
|
||||
@@ -1452,7 +1563,7 @@ describe("ollama plugin", () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const model = { id: "llama3.2", provider: "ollama2", baseUrl: undefined };
|
||||
const model = { id: "llama3.2", provider: "ollama2", api: "ollama", baseUrl: undefined };
|
||||
|
||||
provider.createStreamFn?.({ config, model, provider: "ollama2" } as never);
|
||||
|
||||
@@ -1472,7 +1583,7 @@ describe("ollama plugin", () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const model = { id: "llama3.2", provider: "ollama2", baseUrl: undefined };
|
||||
const model = { id: "llama3.2", provider: "ollama2", api: "ollama", baseUrl: undefined };
|
||||
|
||||
provider.createStreamFn?.({ config, model, provider: "ollama2" } as never);
|
||||
|
||||
@@ -1497,7 +1608,7 @@ describe("ollama plugin", () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const model = { id: "llama3.2", provider: "ollama", baseUrl: undefined };
|
||||
const model = { id: "llama3.2", provider: "ollama", api: "ollama", baseUrl: undefined };
|
||||
|
||||
provider.createStreamFn?.({ config, model, provider: "ollama" } as never);
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
OLLAMA_PROVIDER_ID,
|
||||
isLocalOllamaBaseUrl,
|
||||
resolveOllamaDiscoveryResult,
|
||||
resolveOllamaRuntimeBaseUrl,
|
||||
shouldUseSyntheticOllamaAuth,
|
||||
type OllamaPluginConfig,
|
||||
} from "./src/discovery-shared.js";
|
||||
@@ -104,7 +105,7 @@ function toDynamicOllamaModel(params: {
|
||||
id: params.model.id,
|
||||
name: params.model.name ?? params.model.id,
|
||||
provider: params.provider,
|
||||
api: "ollama",
|
||||
api: params.providerConfig.api ?? "ollama",
|
||||
baseUrl: readProviderBaseUrl(params.providerConfig) ?? "",
|
||||
reasoning: params.model.reasoning ?? false,
|
||||
input: input.length > 0 ? input : ["text"],
|
||||
@@ -475,6 +476,9 @@ export default definePluginEntry({
|
||||
}),
|
||||
},
|
||||
createStreamFn: ({ config, model, provider }) => {
|
||||
if (model.api !== "ollama") {
|
||||
return undefined;
|
||||
}
|
||||
return createConfiguredOllamaStreamFn({
|
||||
model,
|
||||
providerBaseUrl:
|
||||
@@ -597,6 +601,9 @@ export default definePluginEntry({
|
||||
await ensureOllamaModelPulled({ config, model, prompter });
|
||||
},
|
||||
createStreamFn: ({ config, model, provider }) => {
|
||||
if (model.api !== "ollama") {
|
||||
return undefined;
|
||||
}
|
||||
return createConfiguredOllamaStreamFn({
|
||||
model,
|
||||
providerBaseUrl: readProviderBaseUrl(
|
||||
@@ -658,17 +665,27 @@ export default definePluginEntry({
|
||||
}
|
||||
const baseUrl = readProviderBaseUrl(providerConfig);
|
||||
const provider = await buildOllamaProvider(baseUrl, { quiet: true });
|
||||
const dynamicModels = (provider.models ?? []).map((model) =>
|
||||
const dynamicApi = providerConfig?.api ?? provider.api;
|
||||
const dynamicProvider = {
|
||||
...provider,
|
||||
baseUrl: resolveOllamaRuntimeBaseUrl({
|
||||
api: dynamicApi,
|
||||
configuredBaseUrl: baseUrl,
|
||||
discoveredBaseUrl: provider.baseUrl,
|
||||
}),
|
||||
api: dynamicApi,
|
||||
};
|
||||
const dynamicModels = (dynamicProvider.models ?? []).map((model) =>
|
||||
toDynamicOllamaModel({
|
||||
provider: ctx.provider,
|
||||
providerConfig: provider,
|
||||
providerConfig: dynamicProvider,
|
||||
model,
|
||||
}),
|
||||
);
|
||||
if (!dynamicModels.some((model) => model.id === ctx.modelId)) {
|
||||
const requestedModel = await resolveRequestedDynamicOllamaModel({
|
||||
provider: ctx.provider,
|
||||
providerConfig: provider,
|
||||
providerConfig: dynamicProvider,
|
||||
modelId: ctx.modelId,
|
||||
});
|
||||
if (requestedModel) {
|
||||
|
||||
@@ -150,7 +150,7 @@ describe("Ollama provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve explicit ollama baseUrl on implicit provider injection", async () => {
|
||||
it("should preserve explicit ollama baseUrl and api on implicit provider injection", async () => {
|
||||
const fetchMock = stubTagsFetch();
|
||||
|
||||
await withOllamaApiKey(async () => {
|
||||
@@ -171,8 +171,33 @@ describe("Ollama provider", () => {
|
||||
|
||||
expect(countFetchCallUrls(fetchMock, "/api/tags")).toBe(1);
|
||||
|
||||
// Native API strips /v1 suffix via resolveOllamaApiBase()
|
||||
expect(provider?.baseUrl).toBe("http://192.168.20.14:11434/v1");
|
||||
expect(provider?.api).toBe("openai-completions");
|
||||
});
|
||||
});
|
||||
|
||||
it("should normalize explicit native ollama baseUrl on implicit provider injection", async () => {
|
||||
const fetchMock = stubTagsFetch();
|
||||
|
||||
await withOllamaApiKey(async () => {
|
||||
const provider = await runOllamaCatalog({
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
baseUrl: "http://192.168.20.14:11434/v1",
|
||||
api: "ollama",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: { OLLAMA_API_KEY: "test-key" },
|
||||
});
|
||||
|
||||
expect(countFetchCallUrls(fetchMock, "/api/tags")).toBe(1);
|
||||
expect(provider?.baseUrl).toBe("http://192.168.20.14:11434");
|
||||
expect(provider?.api).toBe("ollama");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -650,7 +675,7 @@ describe("Ollama provider", () => {
|
||||
});
|
||||
|
||||
expect(provider?.apiKey).toBe("config-ollama-key");
|
||||
expect(provider?.baseUrl).toBe("http://remote-ollama:11434");
|
||||
expect(provider?.baseUrl).toBe("http://remote-ollama:11434/v1");
|
||||
expect(provider?.api).toBe("openai-completions");
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -42,6 +42,17 @@ function isOllamaApiKeyMarker(value: string): boolean {
|
||||
return value === "OLLAMA_API_KEY" || value === OLLAMA_DEFAULT_API_KEY;
|
||||
}
|
||||
|
||||
export function resolveOllamaRuntimeBaseUrl(params: {
|
||||
api?: ModelProviderConfig["api"];
|
||||
configuredBaseUrl?: string;
|
||||
discoveredBaseUrl: string;
|
||||
}): string {
|
||||
if (params.configuredBaseUrl && params.api && params.api !== "ollama") {
|
||||
return params.configuredBaseUrl;
|
||||
}
|
||||
return params.discoveredBaseUrl;
|
||||
}
|
||||
|
||||
function resolveOllamaDiscoveryApiKey(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
baseUrl?: string;
|
||||
@@ -251,10 +262,12 @@ export async function resolveOllamaDiscoveryResult(params: {
|
||||
ollamaKey.trim() !== OLLAMA_DEFAULT_API_KEY;
|
||||
const explicitApiKey = readStringValue(explicit?.apiKey);
|
||||
if (hasExplicitModels && explicit) {
|
||||
const baseUrl = resolveOllamaApiBase(readProviderBaseUrl(explicit) ?? OLLAMA_DEFAULT_BASE_URL);
|
||||
const configuredBaseUrl = readProviderBaseUrl(explicit) ?? OLLAMA_DEFAULT_BASE_URL;
|
||||
const discoveredBaseUrl = resolveOllamaApiBase(configuredBaseUrl);
|
||||
const api = explicit.api ?? "ollama";
|
||||
const apiKey = resolveOllamaDiscoveryApiKey({
|
||||
env: params.ctx.env,
|
||||
baseUrl,
|
||||
baseUrl: discoveredBaseUrl,
|
||||
explicitApiKey,
|
||||
resolvedApiKey: ollamaKey,
|
||||
resolvedDiscoveryApiKey: ollamaDiscoveryKey,
|
||||
@@ -262,8 +275,8 @@ export async function resolveOllamaDiscoveryResult(params: {
|
||||
return {
|
||||
provider: {
|
||||
...explicit,
|
||||
baseUrl,
|
||||
api: explicit.api ?? "ollama",
|
||||
baseUrl: resolveOllamaRuntimeBaseUrl({ api, configuredBaseUrl, discoveredBaseUrl }),
|
||||
api,
|
||||
...(apiKey ? { apiKey } : {}),
|
||||
},
|
||||
};
|
||||
@@ -307,9 +320,16 @@ export async function resolveOllamaDiscoveryResult(params: {
|
||||
resolvedApiKey: ollamaKey,
|
||||
resolvedDiscoveryApiKey: ollamaDiscoveryKey,
|
||||
});
|
||||
const api = explicit?.api ?? provider.api;
|
||||
return {
|
||||
provider: {
|
||||
...provider,
|
||||
baseUrl: resolveOllamaRuntimeBaseUrl({
|
||||
api,
|
||||
configuredBaseUrl,
|
||||
discoveredBaseUrl: provider.baseUrl,
|
||||
}),
|
||||
api,
|
||||
...(apiKey ? { apiKey } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user