Skip to content

Commit cf9db91

Browse files
laurieluoobviyus
andauthored
fix(web-search): recover OpenRouter Perplexity citations from message annotations (openclaw#40881)
Merged via squash. Prepared head SHA: 66c8bb2 Co-authored-by: laurieluo <[email protected]> Co-authored-by: obviyus <[email protected]> Reviewed-by: @obviyus
1 parent 3822870 commit cf9db91

File tree

3 files changed

+88
-6
lines changed

3 files changed

+88
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai
4848
- 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.
4949
- 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.
5050
- 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.
51+
- Tools/web search: recover OpenRouter Perplexity citation extraction from `message.annotations` when chat-completions responses omit top-level citations. (#40881) Thanks @laurieluo.
5152

5253
## 2026.3.8
5354

src/agents/tools/web-search.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,16 @@ type PerplexitySearchResponse = {
396396
choices?: Array<{
397397
message?: {
398398
content?: string;
399+
annotations?: Array<{
400+
type?: string;
401+
url?: string;
402+
url_citation?: {
403+
url?: string;
404+
title?: string;
405+
start_index?: number;
406+
end_index?: number;
407+
};
408+
}>;
399409
};
400410
}>;
401411
citations?: string[];
@@ -414,6 +424,38 @@ type PerplexitySearchApiResponse = {
414424
id?: string;
415425
};
416426

427+
function extractPerplexityCitations(data: PerplexitySearchResponse): string[] {
428+
const normalizeUrl = (value: unknown): string | undefined => {
429+
if (typeof value !== "string") {
430+
return undefined;
431+
}
432+
const trimmed = value.trim();
433+
return trimmed ? trimmed : undefined;
434+
};
435+
436+
const topLevel = (data.citations ?? [])
437+
.map(normalizeUrl)
438+
.filter((url): url is string => Boolean(url));
439+
if (topLevel.length > 0) {
440+
return [...new Set(topLevel)];
441+
}
442+
443+
const citations: string[] = [];
444+
for (const choice of data.choices ?? []) {
445+
for (const annotation of choice.message?.annotations ?? []) {
446+
if (annotation.type !== "url_citation") {
447+
continue;
448+
}
449+
const url = normalizeUrl(annotation.url_citation?.url ?? annotation.url);
450+
if (url) {
451+
citations.push(url);
452+
}
453+
}
454+
}
455+
456+
return [...new Set(citations)];
457+
}
458+
417459
function extractGrokContent(data: GrokSearchResponse): {
418460
text: string | undefined;
419461
annotationCitations: string[];
@@ -1252,7 +1294,8 @@ async function runPerplexitySearch(params: {
12521294

12531295
const data = (await res.json()) as PerplexitySearchResponse;
12541296
const content = data.choices?.[0]?.message?.content ?? "No response";
1255-
const citations = data.citations ?? [];
1297+
// Prefer top-level citations; fall back to OpenRouter-style message annotations.
1298+
const citations = extractPerplexityCitations(data);
12561299

12571300
return { content, citations };
12581301
},

src/agents/tools/web-tools.enabled-defaults.test.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,13 @@ function installPerplexitySearchApiFetch(results?: Array<Record<string, unknown>
113113
});
114114
}
115115

116-
function installPerplexityChatFetch() {
117-
return installMockFetch({
118-
choices: [{ message: { content: "ok" } }],
119-
citations: ["https://example.com"],
120-
});
116+
function installPerplexityChatFetch(payload?: Record<string, unknown>) {
117+
return installMockFetch(
118+
payload ?? {
119+
choices: [{ message: { content: "ok" } }],
120+
citations: ["https://example.com"],
121+
},
122+
);
121123
}
122124

123125
function createProviderSuccessPayload(
@@ -509,6 +511,42 @@ describe("web_search perplexity OpenRouter compatibility", () => {
509511
expect(body.search_recency_filter).toBe("week");
510512
});
511513

514+
it("falls back to message annotations when top-level citations are missing", async () => {
515+
vi.stubEnv("OPENROUTER_API_KEY", "sk-or-v1-test"); // pragma: allowlist secret
516+
const mockFetch = installPerplexityChatFetch({
517+
choices: [
518+
{
519+
message: {
520+
content: "ok",
521+
annotations: [
522+
{
523+
type: "url_citation",
524+
url_citation: { url: "https://example.com/a" },
525+
},
526+
{
527+
type: "url_citation",
528+
url_citation: { url: "https://example.com/b" },
529+
},
530+
{
531+
type: "url_citation",
532+
url_citation: { url: "https://example.com/a" },
533+
},
534+
],
535+
},
536+
},
537+
],
538+
});
539+
const tool = createPerplexitySearchTool();
540+
const result = await tool?.execute?.("call-1", { query: "test" });
541+
542+
expect(mockFetch).toHaveBeenCalled();
543+
expect(result?.details).toMatchObject({
544+
provider: "perplexity",
545+
citations: ["https://example.com/a", "https://example.com/b"],
546+
content: expect.stringContaining("ok"),
547+
});
548+
});
549+
512550
it("fails loud for Search API-only filters on the compatibility path", async () => {
513551
vi.stubEnv("OPENROUTER_API_KEY", "sk-or-v1-test"); // pragma: allowlist secret
514552
const mockFetch = installPerplexityChatFetch();

0 commit comments

Comments
 (0)