mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-09 05:19:32 +08:00
fix: infer perplexity baseUrl from api key
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
|
||||
Docs: https://docs.clawd.bot
|
||||
|
||||
## 2026.1.20-1
|
||||
|
||||
### Fixes
|
||||
- Web search: infer Perplexity base URL from API key source (direct vs OpenRouter).
|
||||
|
||||
## 2026.1.19-3
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -64,8 +64,11 @@ If both `PERPLEXITY_API_KEY` and `OPENROUTER_API_KEY` are set, set
|
||||
`tools.web.search.perplexity.baseUrl` (or `tools.web.search.perplexity.apiKey`)
|
||||
to disambiguate.
|
||||
|
||||
If `PERPLEXITY_API_KEY` is used from the environment and no base URL is set,
|
||||
Clawdbot defaults to the direct Perplexity endpoint. Set `baseUrl` to override.
|
||||
If no base URL is set, Clawdbot chooses a default based on the API key source:
|
||||
|
||||
- `PERPLEXITY_API_KEY` or `pplx-...` → direct Perplexity (`https://api.perplexity.ai`)
|
||||
- `OPENROUTER_API_KEY` or `sk-or-...` → OpenRouter (`https://openrouter.ai/api/v1`)
|
||||
- Unknown key formats → OpenRouter (safe fallback)
|
||||
|
||||
## Models
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ read_when:
|
||||
|
||||
Clawdbot ships two lightweight web tools:
|
||||
|
||||
- `web_search` — Search the web via Brave Search API (default) or Perplexity Sonar (via OpenRouter).
|
||||
- `web_search` — Search the web via Brave Search API (default) or Perplexity Sonar (direct or via OpenRouter).
|
||||
- `web_fetch` — HTTP fetch + readable extraction (HTML → markdown/text).
|
||||
|
||||
These are **not** browser automation. For JS-heavy sites or logins, use the
|
||||
@@ -31,7 +31,7 @@ These are **not** browser automation. For JS-heavy sites or logins, use the
|
||||
| Provider | Pros | Cons | API Key |
|
||||
|----------|------|------|---------|
|
||||
| **Brave** (default) | Fast, structured results, free tier | Traditional search results | `BRAVE_API_KEY` |
|
||||
| **Perplexity** | AI-synthesized answers, citations, real-time | Requires OpenRouter credits | `OPENROUTER_API_KEY` or `PERPLEXITY_API_KEY` |
|
||||
| **Perplexity** | AI-synthesized answers, citations, real-time | Requires Perplexity or OpenRouter access | `OPENROUTER_API_KEY` or `PERPLEXITY_API_KEY` |
|
||||
|
||||
See [Brave Search setup](/brave-search) and [Perplexity Sonar](/perplexity) for provider-specific details.
|
||||
|
||||
@@ -110,7 +110,7 @@ crypto/prepaid).
|
||||
perplexity: {
|
||||
// API key (optional if OPENROUTER_API_KEY or PERPLEXITY_API_KEY is set)
|
||||
apiKey: "sk-or-v1-...",
|
||||
// Base URL (defaults to OpenRouter)
|
||||
// Base URL (key-aware default if omitted)
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
// Model (defaults to perplexity/sonar-pro)
|
||||
model: "perplexity/sonar-pro"
|
||||
@@ -124,8 +124,11 @@ crypto/prepaid).
|
||||
**Environment alternative:** set `OPENROUTER_API_KEY` or `PERPLEXITY_API_KEY` in the Gateway
|
||||
environment. For a daemon install, put it in `~/.clawdbot/.env`.
|
||||
|
||||
If `PERPLEXITY_API_KEY` is used from the environment and no base URL is set,
|
||||
Clawdbot defaults to the direct Perplexity endpoint (`https://api.perplexity.ai`).
|
||||
If no base URL is set, Clawdbot chooses a default based on the API key source:
|
||||
|
||||
- `PERPLEXITY_API_KEY` or `pplx-...` → `https://api.perplexity.ai`
|
||||
- `OPENROUTER_API_KEY` or `sk-or-...` → `https://openrouter.ai/api/v1`
|
||||
- Unknown key formats → OpenRouter (safe fallback)
|
||||
|
||||
### Available Perplexity models
|
||||
|
||||
|
||||
59
src/agents/tools/web-search.test.ts
Normal file
59
src/agents/tools/web-search.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { __testing } from "./web-search.js";
|
||||
|
||||
const { inferPerplexityBaseUrlFromApiKey, resolvePerplexityBaseUrl } = __testing;
|
||||
|
||||
describe("web_search perplexity baseUrl defaults", () => {
|
||||
it("detects a Perplexity key prefix", () => {
|
||||
expect(inferPerplexityBaseUrlFromApiKey("pplx-123")).toBe("direct");
|
||||
});
|
||||
|
||||
it("detects an OpenRouter key prefix", () => {
|
||||
expect(inferPerplexityBaseUrlFromApiKey("sk-or-v1-123")).toBe("openrouter");
|
||||
});
|
||||
|
||||
it("returns undefined for unknown key formats", () => {
|
||||
expect(inferPerplexityBaseUrlFromApiKey("unknown-key")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("prefers explicit baseUrl over key-based defaults", () => {
|
||||
expect(
|
||||
resolvePerplexityBaseUrl(
|
||||
{ baseUrl: "https://example.com" },
|
||||
"config",
|
||||
"pplx-123",
|
||||
),
|
||||
).toBe("https://example.com");
|
||||
});
|
||||
|
||||
it("defaults to direct when using PERPLEXITY_API_KEY", () => {
|
||||
expect(resolvePerplexityBaseUrl(undefined, "perplexity_env")).toBe(
|
||||
"https://api.perplexity.ai",
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults to OpenRouter when using OPENROUTER_API_KEY", () => {
|
||||
expect(resolvePerplexityBaseUrl(undefined, "openrouter_env")).toBe(
|
||||
"https://openrouter.ai/api/v1",
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults to direct when config key looks like Perplexity", () => {
|
||||
expect(resolvePerplexityBaseUrl(undefined, "config", "pplx-123")).toBe(
|
||||
"https://api.perplexity.ai",
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults to OpenRouter when config key looks like OpenRouter", () => {
|
||||
expect(resolvePerplexityBaseUrl(undefined, "config", "sk-or-v1-123")).toBe(
|
||||
"https://openrouter.ai/api/v1",
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults to OpenRouter for unknown config key formats", () => {
|
||||
expect(resolvePerplexityBaseUrl(undefined, "config", "weird-key")).toBe(
|
||||
"https://openrouter.ai/api/v1",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,8 @@ const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
|
||||
const DEFAULT_PERPLEXITY_BASE_URL = "https://openrouter.ai/api/v1";
|
||||
const PERPLEXITY_DIRECT_BASE_URL = "https://api.perplexity.ai";
|
||||
const DEFAULT_PERPLEXITY_MODEL = "perplexity/sonar-pro";
|
||||
const PERPLEXITY_KEY_PREFIXES = ["pplx-"];
|
||||
const OPENROUTER_KEY_PREFIXES = ["sk-or-"];
|
||||
|
||||
const SEARCH_CACHE = new Map<string, CacheEntry<Record<string, unknown>>>();
|
||||
|
||||
@@ -90,6 +92,8 @@ type PerplexitySearchResponse = {
|
||||
citations?: string[];
|
||||
};
|
||||
|
||||
type PerplexityBaseUrlHint = "direct" | "openrouter";
|
||||
|
||||
function resolveSearchConfig(cfg?: ClawdbotConfig): WebSearchConfig {
|
||||
const search = cfg?.tools?.web?.search;
|
||||
if (!search || typeof search !== "object") return undefined;
|
||||
@@ -147,20 +151,17 @@ function resolvePerplexityApiKey(perplexity?: PerplexityConfig): {
|
||||
apiKey?: string;
|
||||
source: PerplexityApiKeySource;
|
||||
} {
|
||||
const fromConfig =
|
||||
perplexity && "apiKey" in perplexity && typeof perplexity.apiKey === "string"
|
||||
? perplexity.apiKey.trim()
|
||||
: "";
|
||||
const fromConfig = normalizeApiKey(perplexity?.apiKey);
|
||||
if (fromConfig) {
|
||||
return { apiKey: fromConfig, source: "config" };
|
||||
}
|
||||
|
||||
const fromEnvPerplexity = (process.env.PERPLEXITY_API_KEY ?? "").trim();
|
||||
const fromEnvPerplexity = normalizeApiKey(process.env.PERPLEXITY_API_KEY);
|
||||
if (fromEnvPerplexity) {
|
||||
return { apiKey: fromEnvPerplexity, source: "perplexity_env" };
|
||||
}
|
||||
|
||||
const fromEnvOpenRouter = (process.env.OPENROUTER_API_KEY ?? "").trim();
|
||||
const fromEnvOpenRouter = normalizeApiKey(process.env.OPENROUTER_API_KEY);
|
||||
if (fromEnvOpenRouter) {
|
||||
return { apiKey: fromEnvOpenRouter, source: "openrouter_env" };
|
||||
}
|
||||
@@ -168,9 +169,26 @@ function resolvePerplexityApiKey(perplexity?: PerplexityConfig): {
|
||||
return { apiKey: undefined, source: "none" };
|
||||
}
|
||||
|
||||
function normalizeApiKey(key: unknown): string {
|
||||
return typeof key === "string" ? key.trim() : "";
|
||||
}
|
||||
|
||||
function inferPerplexityBaseUrlFromApiKey(apiKey?: string): PerplexityBaseUrlHint | undefined {
|
||||
if (!apiKey) return undefined;
|
||||
const normalized = apiKey.toLowerCase();
|
||||
if (PERPLEXITY_KEY_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
||||
return "direct";
|
||||
}
|
||||
if (OPENROUTER_KEY_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
||||
return "openrouter";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolvePerplexityBaseUrl(
|
||||
perplexity?: PerplexityConfig,
|
||||
apiKeySource: PerplexityApiKeySource = "none",
|
||||
apiKey?: string,
|
||||
): string {
|
||||
const fromConfig =
|
||||
perplexity && "baseUrl" in perplexity && typeof perplexity.baseUrl === "string"
|
||||
@@ -178,6 +196,12 @@ function resolvePerplexityBaseUrl(
|
||||
: "";
|
||||
if (fromConfig) return fromConfig;
|
||||
if (apiKeySource === "perplexity_env") return PERPLEXITY_DIRECT_BASE_URL;
|
||||
if (apiKeySource === "openrouter_env") return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
if (apiKeySource === "config") {
|
||||
const inferred = inferPerplexityBaseUrlFromApiKey(apiKey);
|
||||
if (inferred === "direct") return PERPLEXITY_DIRECT_BASE_URL;
|
||||
if (inferred === "openrouter") return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
}
|
||||
return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
}
|
||||
|
||||
@@ -385,10 +409,19 @@ export function createWebSearchTool(options?: {
|
||||
country,
|
||||
search_lang,
|
||||
ui_lang,
|
||||
perplexityBaseUrl: resolvePerplexityBaseUrl(perplexityConfig, perplexityAuth?.source),
|
||||
perplexityBaseUrl: resolvePerplexityBaseUrl(
|
||||
perplexityConfig,
|
||||
perplexityAuth?.source,
|
||||
perplexityAuth?.apiKey,
|
||||
),
|
||||
perplexityModel: resolvePerplexityModel(perplexityConfig),
|
||||
});
|
||||
return jsonResult(result);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
inferPerplexityBaseUrlFromApiKey,
|
||||
resolvePerplexityBaseUrl,
|
||||
} as const;
|
||||
|
||||
@@ -194,7 +194,7 @@ describe("web_search perplexity baseUrl defaults", () => {
|
||||
expect(mockFetch.mock.calls[0]?.[0]).toBe("https://example.com/pplx/chat/completions");
|
||||
});
|
||||
|
||||
it("defaults to OpenRouter when apiKey is configured without baseUrl", async () => {
|
||||
it("defaults to Perplexity direct when apiKey looks like Perplexity", async () => {
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
@@ -219,6 +219,35 @@ describe("web_search perplexity baseUrl defaults", () => {
|
||||
});
|
||||
await tool?.execute?.(1, { query: "test-config-apikey" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
expect(mockFetch.mock.calls[0]?.[0]).toBe("https://api.perplexity.ai/chat/completions");
|
||||
});
|
||||
|
||||
it("defaults to OpenRouter when apiKey looks like OpenRouter", async () => {
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ choices: [{ message: { content: "ok" } }], citations: [] }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "perplexity",
|
||||
perplexity: { apiKey: "sk-or-v1-test" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxed: true,
|
||||
});
|
||||
await tool?.execute?.(1, { query: "test-openrouter-config" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
expect(mockFetch.mock.calls[0]?.[0]).toBe("https://openrouter.ai/api/v1/chat/completions");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user