mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(web-search): recover OpenRouter Perplexity citations from message annotations (#40881)
Merged via squash.
Prepared head SHA: 66c8bb2c6a
Co-authored-by: laurieluo <89195476+laurieluo@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
@@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/Sessions: restore single-column session table collapse on narrow viewport or container widths by moving the responsive table override next to the base grid rule and enabling inline-size container queries. (#12175) Thanks @benjipeng.
|
||||
- Telegram/final preview delivery: split active preview lifecycle from cleanup retention so missing archived preview edits avoid duplicate fallback sends without clearing the live preview or blocking later in-place finalization. (#41662) thanks @hougangdev.
|
||||
- Cron/state errors: record `lastErrorReason` in cron job state and keep the gateway schema aligned with the full failover-reason set, including regression coverage for protocol conformance. (#14382) thanks @futuremind2026.
|
||||
- Tools/web search: recover OpenRouter Perplexity citation extraction from `message.annotations` when chat-completions responses omit top-level citations. (#40881) Thanks @laurieluo.
|
||||
|
||||
## 2026.3.8
|
||||
|
||||
|
||||
@@ -396,6 +396,16 @@ type PerplexitySearchResponse = {
|
||||
choices?: Array<{
|
||||
message?: {
|
||||
content?: string;
|
||||
annotations?: Array<{
|
||||
type?: string;
|
||||
url?: string;
|
||||
url_citation?: {
|
||||
url?: string;
|
||||
title?: string;
|
||||
start_index?: number;
|
||||
end_index?: number;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
citations?: string[];
|
||||
@@ -414,6 +424,38 @@ type PerplexitySearchApiResponse = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
function extractPerplexityCitations(data: PerplexitySearchResponse): string[] {
|
||||
const normalizeUrl = (value: unknown): string | undefined => {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
};
|
||||
|
||||
const topLevel = (data.citations ?? [])
|
||||
.map(normalizeUrl)
|
||||
.filter((url): url is string => Boolean(url));
|
||||
if (topLevel.length > 0) {
|
||||
return [...new Set(topLevel)];
|
||||
}
|
||||
|
||||
const citations: string[] = [];
|
||||
for (const choice of data.choices ?? []) {
|
||||
for (const annotation of choice.message?.annotations ?? []) {
|
||||
if (annotation.type !== "url_citation") {
|
||||
continue;
|
||||
}
|
||||
const url = normalizeUrl(annotation.url_citation?.url ?? annotation.url);
|
||||
if (url) {
|
||||
citations.push(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(citations)];
|
||||
}
|
||||
|
||||
function extractGrokContent(data: GrokSearchResponse): {
|
||||
text: string | undefined;
|
||||
annotationCitations: string[];
|
||||
@@ -1252,7 +1294,8 @@ async function runPerplexitySearch(params: {
|
||||
|
||||
const data = (await res.json()) as PerplexitySearchResponse;
|
||||
const content = data.choices?.[0]?.message?.content ?? "No response";
|
||||
const citations = data.citations ?? [];
|
||||
// Prefer top-level citations; fall back to OpenRouter-style message annotations.
|
||||
const citations = extractPerplexityCitations(data);
|
||||
|
||||
return { content, citations };
|
||||
},
|
||||
|
||||
@@ -113,11 +113,13 @@ function installPerplexitySearchApiFetch(results?: Array<Record<string, unknown>
|
||||
});
|
||||
}
|
||||
|
||||
function installPerplexityChatFetch() {
|
||||
return installMockFetch({
|
||||
choices: [{ message: { content: "ok" } }],
|
||||
citations: ["https://example.com"],
|
||||
});
|
||||
function installPerplexityChatFetch(payload?: Record<string, unknown>) {
|
||||
return installMockFetch(
|
||||
payload ?? {
|
||||
choices: [{ message: { content: "ok" } }],
|
||||
citations: ["https://example.com"],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createProviderSuccessPayload(
|
||||
@@ -509,6 +511,42 @@ describe("web_search perplexity OpenRouter compatibility", () => {
|
||||
expect(body.search_recency_filter).toBe("week");
|
||||
});
|
||||
|
||||
it("falls back to message annotations when top-level citations are missing", async () => {
|
||||
vi.stubEnv("OPENROUTER_API_KEY", "sk-or-v1-test"); // pragma: allowlist secret
|
||||
const mockFetch = installPerplexityChatFetch({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: "ok",
|
||||
annotations: [
|
||||
{
|
||||
type: "url_citation",
|
||||
url_citation: { url: "https://example.com/a" },
|
||||
},
|
||||
{
|
||||
type: "url_citation",
|
||||
url_citation: { url: "https://example.com/b" },
|
||||
},
|
||||
{
|
||||
type: "url_citation",
|
||||
url_citation: { url: "https://example.com/a" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const tool = createPerplexitySearchTool();
|
||||
const result = await tool?.execute?.("call-1", { query: "test" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
expect(result?.details).toMatchObject({
|
||||
provider: "perplexity",
|
||||
citations: ["https://example.com/a", "https://example.com/b"],
|
||||
content: expect.stringContaining("ok"),
|
||||
});
|
||||
});
|
||||
|
||||
it("fails loud for Search API-only filters on the compatibility path", async () => {
|
||||
vi.stubEnv("OPENROUTER_API_KEY", "sk-or-v1-test"); // pragma: allowlist secret
|
||||
const mockFetch = installPerplexityChatFetch();
|
||||
|
||||
Reference in New Issue
Block a user