fix: infer perplexity baseUrl from api key
This commit is contained in:
parent
52e9450a79
commit
3e546e691d
@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
Docs: https://docs.clawd.bot
|
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
|
## 2026.1.19-3
|
||||||
|
|
||||||
### Changes
|
### 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`)
|
`tools.web.search.perplexity.baseUrl` (or `tools.web.search.perplexity.apiKey`)
|
||||||
to disambiguate.
|
to disambiguate.
|
||||||
|
|
||||||
If `PERPLEXITY_API_KEY` is used from the environment and no base URL is set,
|
If no base URL is set, Clawdbot chooses a default based on the API key source:
|
||||||
Clawdbot defaults to the direct Perplexity endpoint. Set `baseUrl` to override.
|
|
||||||
|
- `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
|
## Models
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ read_when:
|
|||||||
|
|
||||||
Clawdbot ships two lightweight web tools:
|
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).
|
- `web_fetch` — HTTP fetch + readable extraction (HTML → markdown/text).
|
||||||
|
|
||||||
These are **not** browser automation. For JS-heavy sites or logins, use the
|
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 |
|
| Provider | Pros | Cons | API Key |
|
||||||
|----------|------|------|---------|
|
|----------|------|------|---------|
|
||||||
| **Brave** (default) | Fast, structured results, free tier | Traditional search results | `BRAVE_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.
|
See [Brave Search setup](/brave-search) and [Perplexity Sonar](/perplexity) for provider-specific details.
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ crypto/prepaid).
|
|||||||
perplexity: {
|
perplexity: {
|
||||||
// API key (optional if OPENROUTER_API_KEY or PERPLEXITY_API_KEY is set)
|
// API key (optional if OPENROUTER_API_KEY or PERPLEXITY_API_KEY is set)
|
||||||
apiKey: "sk-or-v1-...",
|
apiKey: "sk-or-v1-...",
|
||||||
// Base URL (defaults to OpenRouter)
|
// Base URL (key-aware default if omitted)
|
||||||
baseUrl: "https://openrouter.ai/api/v1",
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
// Model (defaults to perplexity/sonar-pro)
|
// Model (defaults to perplexity/sonar-pro)
|
||||||
model: "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 alternative:** set `OPENROUTER_API_KEY` or `PERPLEXITY_API_KEY` in the Gateway
|
||||||
environment. For a daemon install, put it in `~/.clawdbot/.env`.
|
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,
|
If no base URL is set, Clawdbot chooses a default based on the API key source:
|
||||||
Clawdbot defaults to the direct Perplexity endpoint (`https://api.perplexity.ai`).
|
|
||||||
|
- `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
|
### 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 DEFAULT_PERPLEXITY_BASE_URL = "https://openrouter.ai/api/v1";
|
||||||
const PERPLEXITY_DIRECT_BASE_URL = "https://api.perplexity.ai";
|
const PERPLEXITY_DIRECT_BASE_URL = "https://api.perplexity.ai";
|
||||||
const DEFAULT_PERPLEXITY_MODEL = "perplexity/sonar-pro";
|
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>>>();
|
const SEARCH_CACHE = new Map<string, CacheEntry<Record<string, unknown>>>();
|
||||||
|
|
||||||
@ -90,6 +92,8 @@ type PerplexitySearchResponse = {
|
|||||||
citations?: string[];
|
citations?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PerplexityBaseUrlHint = "direct" | "openrouter";
|
||||||
|
|
||||||
function resolveSearchConfig(cfg?: ClawdbotConfig): WebSearchConfig {
|
function resolveSearchConfig(cfg?: ClawdbotConfig): WebSearchConfig {
|
||||||
const search = cfg?.tools?.web?.search;
|
const search = cfg?.tools?.web?.search;
|
||||||
if (!search || typeof search !== "object") return undefined;
|
if (!search || typeof search !== "object") return undefined;
|
||||||
@ -147,20 +151,17 @@ function resolvePerplexityApiKey(perplexity?: PerplexityConfig): {
|
|||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
source: PerplexityApiKeySource;
|
source: PerplexityApiKeySource;
|
||||||
} {
|
} {
|
||||||
const fromConfig =
|
const fromConfig = normalizeApiKey(perplexity?.apiKey);
|
||||||
perplexity && "apiKey" in perplexity && typeof perplexity.apiKey === "string"
|
|
||||||
? perplexity.apiKey.trim()
|
|
||||||
: "";
|
|
||||||
if (fromConfig) {
|
if (fromConfig) {
|
||||||
return { apiKey: fromConfig, source: "config" };
|
return { apiKey: fromConfig, source: "config" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromEnvPerplexity = (process.env.PERPLEXITY_API_KEY ?? "").trim();
|
const fromEnvPerplexity = normalizeApiKey(process.env.PERPLEXITY_API_KEY);
|
||||||
if (fromEnvPerplexity) {
|
if (fromEnvPerplexity) {
|
||||||
return { apiKey: fromEnvPerplexity, source: "perplexity_env" };
|
return { apiKey: fromEnvPerplexity, source: "perplexity_env" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromEnvOpenRouter = (process.env.OPENROUTER_API_KEY ?? "").trim();
|
const fromEnvOpenRouter = normalizeApiKey(process.env.OPENROUTER_API_KEY);
|
||||||
if (fromEnvOpenRouter) {
|
if (fromEnvOpenRouter) {
|
||||||
return { apiKey: fromEnvOpenRouter, source: "openrouter_env" };
|
return { apiKey: fromEnvOpenRouter, source: "openrouter_env" };
|
||||||
}
|
}
|
||||||
@ -168,9 +169,26 @@ function resolvePerplexityApiKey(perplexity?: PerplexityConfig): {
|
|||||||
return { apiKey: undefined, source: "none" };
|
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(
|
function resolvePerplexityBaseUrl(
|
||||||
perplexity?: PerplexityConfig,
|
perplexity?: PerplexityConfig,
|
||||||
apiKeySource: PerplexityApiKeySource = "none",
|
apiKeySource: PerplexityApiKeySource = "none",
|
||||||
|
apiKey?: string,
|
||||||
): string {
|
): string {
|
||||||
const fromConfig =
|
const fromConfig =
|
||||||
perplexity && "baseUrl" in perplexity && typeof perplexity.baseUrl === "string"
|
perplexity && "baseUrl" in perplexity && typeof perplexity.baseUrl === "string"
|
||||||
@ -178,6 +196,12 @@ function resolvePerplexityBaseUrl(
|
|||||||
: "";
|
: "";
|
||||||
if (fromConfig) return fromConfig;
|
if (fromConfig) return fromConfig;
|
||||||
if (apiKeySource === "perplexity_env") return PERPLEXITY_DIRECT_BASE_URL;
|
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;
|
return DEFAULT_PERPLEXITY_BASE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,10 +409,19 @@ export function createWebSearchTool(options?: {
|
|||||||
country,
|
country,
|
||||||
search_lang,
|
search_lang,
|
||||||
ui_lang,
|
ui_lang,
|
||||||
perplexityBaseUrl: resolvePerplexityBaseUrl(perplexityConfig, perplexityAuth?.source),
|
perplexityBaseUrl: resolvePerplexityBaseUrl(
|
||||||
|
perplexityConfig,
|
||||||
|
perplexityAuth?.source,
|
||||||
|
perplexityAuth?.apiKey,
|
||||||
|
),
|
||||||
perplexityModel: resolvePerplexityModel(perplexityConfig),
|
perplexityModel: resolvePerplexityModel(perplexityConfig),
|
||||||
});
|
});
|
||||||
return jsonResult(result);
|
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");
|
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(() =>
|
const mockFetch = vi.fn(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
@ -219,6 +219,35 @@ describe("web_search perplexity baseUrl defaults", () => {
|
|||||||
});
|
});
|
||||||
await tool?.execute?.(1, { query: "test-config-apikey" });
|
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).toHaveBeenCalled();
|
||||||
expect(mockFetch.mock.calls[0]?.[0]).toBe("https://openrouter.ai/api/v1/chat/completions");
|
expect(mockFetch.mock.calls[0]?.[0]).toBe("https://openrouter.ai/api/v1/chat/completions");
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user