fix(ollama): align web search endpoint routing

This commit is contained in:
Peter Steinberger
2026-04-27 01:10:23 +01:00
parent b825c8d34b
commit 0f672dcc73
4 changed files with 92 additions and 30 deletions

View File

@@ -125,7 +125,7 @@ describe("ollama web search provider", () => {
).toBe("https://ollama.com");
});
it("maps generic search args into the Ollama search endpoint", async () => {
it("maps generic search args into the local Ollama proxy endpoint", async () => {
const release = vi.fn(async () => {});
fetchWithSsrFGuardMock.mockResolvedValue({
response: new Response(
@@ -157,7 +157,7 @@ describe("ollama web search provider", () => {
expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "http://ollama.local:11434/api/web_search",
url: "http://ollama.local:11434/api/experimental/web_search",
auditContext: "ollama-web-search.search",
}),
);
@@ -184,7 +184,7 @@ describe("ollama web search provider", () => {
expect(release).toHaveBeenCalledTimes(1);
});
it("falls back to the legacy Ollama web search endpoint when /api/web_search is missing", async () => {
it("tries the future local direct endpoint when the local proxy endpoint is missing", async () => {
fetchWithSsrFGuardMock
.mockResolvedValueOnce({
response: new Response("not found", { status: 404 }),
@@ -211,11 +211,42 @@ describe("ollama web search provider", () => {
});
expect(fetchWithSsrFGuardMock.mock.calls.map((call) => call[0].url)).toEqual([
"http://ollama.local:11434/api/web_search",
"http://ollama.local:11434/api/experimental/web_search",
"http://ollama.local:11434/api/web_search",
]);
});
it("uses only the hosted endpoint for Ollama Cloud base URLs", async () => {
fetchWithSsrFGuardMock.mockResolvedValueOnce({
response: new Response(
JSON.stringify({
results: [{ title: "Cloud", url: "https://example.com", content: "result" }],
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
),
release: vi.fn(async () => {}),
});
await expect(
runOllamaWebSearch({
config: createOllamaConfig({
baseUrl: "https://ollama.com",
apiKey: "cloud-config-secret",
}),
query: "openclaw",
}),
).resolves.toMatchObject({ count: 1 });
expect(fetchWithSsrFGuardMock.mock.calls).toHaveLength(1);
expect(fetchWithSsrFGuardMock.mock.calls[0]?.[0].url).toBe("https://ollama.com/api/web_search");
expect(fetchWithSsrFGuardMock.mock.calls[0]?.[0].init?.headers).toMatchObject({
Authorization: "Bearer cloud-config-secret",
});
});
it("uses an env Ollama key only for the cloud fallback from a local host", async () => {
const original = process.env.OLLAMA_API_KEY;
try {
@@ -256,6 +287,11 @@ describe("ollama web search provider", () => {
| undefined;
expect(firstHeaders?.Authorization).toBeUndefined();
expect(cloudHeaders?.Authorization).toBe("Bearer cloud-secret");
expect(fetchWithSsrFGuardMock.mock.calls.map((call) => call[0].url)).toEqual([
"http://ollama.local:11434/api/experimental/web_search",
"http://ollama.local:11434/api/web_search",
"https://ollama.com/api/web_search",
]);
expect(fetchWithSsrFGuardMock.mock.calls[2]?.[0].url).toBe(
"https://ollama.com/api/web_search",
);

View File

@@ -41,8 +41,8 @@ const OLLAMA_WEB_SEARCH_SCHEMA = Type.Object(
{ additionalProperties: false },
);
const OLLAMA_WEB_SEARCH_PATH = "/api/web_search";
const OLLAMA_LEGACY_WEB_SEARCH_PATH = "/api/experimental/web_search";
const OLLAMA_HOSTED_WEB_SEARCH_PATH = "/api/web_search";
const OLLAMA_LOCAL_WEB_SEARCH_PROXY_PATH = "/api/experimental/web_search";
const OLLAMA_CLOUD_BASE_URL = "https://ollama.com";
const DEFAULT_OLLAMA_WEB_SEARCH_COUNT = 5;
const DEFAULT_OLLAMA_WEB_SEARCH_TIMEOUT_MS = 15_000;
@@ -58,6 +58,12 @@ type OllamaWebSearchResponse = {
results?: OllamaWebSearchResult[];
};
type OllamaWebSearchAttempt = {
baseUrl: string;
path: string;
apiKey?: string;
};
function isOllamaCloudBaseUrl(baseUrl: string): boolean {
try {
const parsed = new URL(baseUrl);
@@ -111,6 +117,43 @@ function normalizeOllamaWebSearchResult(
};
}
function buildOllamaWebSearchAttempts(params: {
baseUrl: string;
configuredApiKey?: string;
envApiKey?: string;
}): OllamaWebSearchAttempt[] {
if (isOllamaCloudBaseUrl(params.baseUrl)) {
return [
{
baseUrl: params.baseUrl,
path: OLLAMA_HOSTED_WEB_SEARCH_PATH,
apiKey: params.configuredApiKey ?? params.envApiKey,
},
];
}
const attempts: OllamaWebSearchAttempt[] = [
{
baseUrl: params.baseUrl,
path: OLLAMA_LOCAL_WEB_SEARCH_PROXY_PATH,
apiKey: params.configuredApiKey,
},
{
baseUrl: params.baseUrl,
path: OLLAMA_HOSTED_WEB_SEARCH_PATH,
apiKey: params.configuredApiKey,
},
];
if (params.envApiKey) {
attempts.push({
baseUrl: OLLAMA_CLOUD_BASE_URL,
path: OLLAMA_HOSTED_WEB_SEARCH_PATH,
apiKey: params.envApiKey,
});
}
return attempts;
}
export async function runOllamaWebSearch(params: {
config?: OpenClawConfig;
query: string;
@@ -127,27 +170,7 @@ export async function runOllamaWebSearch(params: {
const count = resolveSearchCount(params.count, DEFAULT_OLLAMA_WEB_SEARCH_COUNT);
const startedAt = Date.now();
const body = JSON.stringify({ query, max_results: count });
const attempts = [
{
baseUrl,
path: OLLAMA_WEB_SEARCH_PATH,
apiKey: isOllamaCloudBaseUrl(baseUrl) ? (configuredApiKey ?? envApiKey) : configuredApiKey,
},
{
baseUrl,
path: OLLAMA_LEGACY_WEB_SEARCH_PATH,
apiKey: isOllamaCloudBaseUrl(baseUrl) ? (configuredApiKey ?? envApiKey) : configuredApiKey,
},
...(!isOllamaCloudBaseUrl(baseUrl) && envApiKey
? [
{
baseUrl: OLLAMA_CLOUD_BASE_URL,
path: OLLAMA_WEB_SEARCH_PATH,
apiKey: envApiKey,
},
]
: []),
];
const attempts = buildOllamaWebSearchAttempts({ baseUrl, configuredApiKey, envApiKey });
let payload: OllamaWebSearchResponse | undefined;
let lastError: Error | undefined;
@@ -305,6 +328,7 @@ export function createOllamaWebSearchProvider(): WebSearchProviderPlugin {
}
export const __testing = {
buildOllamaWebSearchAttempts,
normalizeOllamaWebSearchResult,
resolveConfiguredOllamaWebSearchApiKey,
resolveEnvOllamaWebSearchApiKey,