mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:20:44 +00:00
fix(ollama): align web search endpoint routing
This commit is contained in:
@@ -21,7 +21,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/Ollama: expose native Ollama thinking effort levels so `/think max` is accepted for reasoning-capable Ollama models and maps to Ollama's highest supported `think` effort. Fixes #71584. Thanks @g0st1n.
|
||||
- Providers/Ollama: strip the active custom Ollama provider prefix before native chat and embedding requests, so custom provider ids like `ollama-spark/qwen3:32b` reach Ollama as the real model name. Fixes #72353. Thanks @maximus-dss and @hclsys.
|
||||
- Providers/Ollama: move memory embeddings to Ollama's current `/api/embed` endpoint with batched `input` requests while preserving vector normalization and custom provider auth/header overrides. Fixes #39983. Thanks @sskkcc and @LiudengZhang.
|
||||
- Providers/Ollama: try both current and legacy Ollama web-search endpoints and use `OLLAMA_API_KEY` only for the `ollama.com` cloud fallback, keeping local signed-in hosts keyless. Fixes #69132. Thanks @yoon1012 and @hyspacex.
|
||||
- Providers/Ollama: route local web search through Ollama's signed `/api/experimental/web_search` daemon proxy, use hosted `/api/web_search` directly for `ollama.com`, and keep `OLLAMA_API_KEY` scoped to cloud fallback auth. Fixes #69132. Thanks @yoon1012 and @hyspacex.
|
||||
- Agents/Ollama: apply provider-owned replay turn normalization to native Ollama chat so Cloud models no longer reject non-alternating replay history in agent/Gateway runs. Fixes #71697. Thanks @ismael-81.
|
||||
- Agents/Ollama: validate explicit `--thinking max` against catalog-discovered Ollama reasoning metadata so local agent runs accept the same native thinking levels shown in the model catalog. Fixes #71584. Thanks @g0st1n.
|
||||
- Docker/QA: add observability coverage to the normal Docker aggregate so QA-lab OTEL and Prometheus diagnostics run inside Docker. Thanks @vincentkoc.
|
||||
|
||||
@@ -92,8 +92,10 @@ for requests to that configured host.
|
||||
it does not block selection.
|
||||
- Runtime auto-detect can fall back to Ollama Web Search when no higher-priority
|
||||
credentialed provider is configured.
|
||||
- The provider tries Ollama's `/api/web_search` endpoint first, then the legacy
|
||||
`/api/experimental/web_search` endpoint for older hosts.
|
||||
- Local Ollama daemon hosts use the local proxy endpoint
|
||||
`/api/experimental/web_search`, which signs and forwards to Ollama Cloud.
|
||||
- `https://ollama.com` hosts use the public hosted endpoint
|
||||
`/api/web_search` directly with bearer API-key auth.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user