feat(coding-agent/web): added web search CLI subcommand and refactored provider management
- Added new `omp q` CLI subcommand for testing web search providers with query, provider, recency, and limit options. - Added web search provider information API with centralized provider-info module supporting authentication requirements and provider metadata. - Added support for `hour` recency filter option in Perplexity web search. - Refactored web search provider definitions into centralized provider-info module for improved maintainability. - Updated web search result rendering to support long-form answers with text wrapping in CLI mode. - Removed related questions section from web search result rendering.
This commit is contained in:
@@ -1,14 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added `omp q` CLI subcommand for testing web search providers with query, provider, recency, and limit options
|
||||
- Added web search provider information API with authentication requirements and provider metadata
|
||||
- Added support for `hour` recency filter option in Perplexity web search
|
||||
- Support for image file mentions—images are now automatically detected, resized, and attached when referenced with @filepath syntax
|
||||
- Image dimension information displayed in file mention UI to show image properties alongside text files
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactored web search provider definitions into centralized provider-info module for better maintainability
|
||||
- Updated web search result rendering to support long-form answers with text wrapping in CLI mode
|
||||
- Removed related questions section from web search result rendering
|
||||
- Updated Perplexity API types to support extended message content formats including images, files, and PDFs
|
||||
- Updated Perplexity search to use 'pro' search type for improved search quality and relevance
|
||||
- File mention messages now support both text content and image attachments, with optional line count for text files
|
||||
- Updated file mention processing to respect image auto-resize settings
|
||||
|
||||
@@ -196,6 +196,7 @@ ${chalk.bold("Subcommands:")}
|
||||
jupyter Manage the shared Jupyter gateway
|
||||
shell Interactive shell console (brush-core test)
|
||||
grep Test grep tool
|
||||
q Test web search providers
|
||||
|
||||
${chalk.bold("Options:")}
|
||||
--model <pattern> Model to use (fuzzy match: "opus", "gpt-5.2", or "p-openai/gpt-5.2")
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Web search CLI command handlers.
|
||||
*
|
||||
* Handles `omp q`/`omp web-search` subcommands for testing web search providers.
|
||||
*/
|
||||
import chalk from "chalk";
|
||||
import { APP_NAME } from "../config";
|
||||
import { initTheme, theme } from "../modes/theme/theme";
|
||||
import { renderWebSearchResult } from "../web/search/render";
|
||||
import { runWebSearchQuery, type WebSearchParams } from "../web/search/index";
|
||||
import type { WebSearchProvider } from "../web/search/types";
|
||||
|
||||
export interface WebSearchCommandArgs {
|
||||
query: string;
|
||||
provider?: WebSearchProvider | "auto";
|
||||
recency?: "day" | "week" | "month" | "year";
|
||||
limit?: number;
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
const PROVIDERS: Array<WebSearchProvider | "auto"> = [
|
||||
"auto",
|
||||
"anthropic",
|
||||
"perplexity",
|
||||
"exa",
|
||||
"jina",
|
||||
"gemini",
|
||||
"codex",
|
||||
];
|
||||
|
||||
const RECENCY_OPTIONS: WebSearchCommandArgs["recency"][] = ["day", "week", "month", "year"];
|
||||
|
||||
/**
|
||||
* Parse web search subcommand arguments.
|
||||
* Returns undefined if not a web search command.
|
||||
*/
|
||||
export function parseWebSearchArgs(args: string[]): WebSearchCommandArgs | undefined {
|
||||
if (args.length === 0 || (args[0] !== "q" && args[0] !== "web-search")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result: WebSearchCommandArgs = {
|
||||
query: "",
|
||||
expanded: true,
|
||||
};
|
||||
|
||||
const positional: string[] = [];
|
||||
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === "--provider") {
|
||||
result.provider = args[++i] as WebSearchCommandArgs["provider"];
|
||||
} else if (arg === "--recency") {
|
||||
result.recency = args[++i] as WebSearchCommandArgs["recency"];
|
||||
} else if (arg === "--limit" || arg === "-l") {
|
||||
result.limit = Number.parseInt(args[++i], 10);
|
||||
} else if (arg === "--compact") {
|
||||
result.expanded = false;
|
||||
} else if (!arg.startsWith("-")) {
|
||||
positional.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
if (positional.length > 0) {
|
||||
result.query = positional.join(" ");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runWebSearchCommand(cmd: WebSearchCommandArgs): Promise<void> {
|
||||
if (!cmd.query) {
|
||||
writeStderr(chalk.red("Error: Query is required"));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (cmd.provider && !PROVIDERS.includes(cmd.provider)) {
|
||||
writeStderr(chalk.red(`Error: Unknown provider "${cmd.provider}"`));
|
||||
writeStderr(chalk.dim(`Valid providers: ${PROVIDERS.join(", ")}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (cmd.recency && !RECENCY_OPTIONS.includes(cmd.recency)) {
|
||||
writeStderr(chalk.red(`Error: Invalid recency "${cmd.recency}"`));
|
||||
writeStderr(chalk.dim(`Valid recency values: ${RECENCY_OPTIONS.join(", ")}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (cmd.limit !== undefined && Number.isNaN(cmd.limit)) {
|
||||
writeStderr(chalk.red("Error: --limit must be a number"));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await initTheme();
|
||||
|
||||
const params: WebSearchParams = {
|
||||
query: cmd.query,
|
||||
provider: cmd.provider,
|
||||
recency: cmd.recency,
|
||||
limit: cmd.limit,
|
||||
};
|
||||
|
||||
const result = await runWebSearchQuery(params);
|
||||
const component = renderWebSearchResult(result, { expanded: cmd.expanded, isPartial: false }, theme, {
|
||||
query: cmd.query,
|
||||
provider: cmd.provider,
|
||||
allowLongAnswer: true,
|
||||
maxAnswerLines: cmd.expanded ? undefined : 6,
|
||||
});
|
||||
|
||||
const width = Math.max(60, process.stdout.columns ?? 100);
|
||||
writeStdout(component.render(width).join("\n"));
|
||||
|
||||
if (result.details?.error) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function printWebSearchHelp(): void {
|
||||
writeStdout(`${chalk.bold(`${APP_NAME} q`)} - Test web search providers
|
||||
|
||||
${chalk.bold("Usage:")}
|
||||
${APP_NAME} q [options] <query>
|
||||
${APP_NAME} web-search [options] <query>
|
||||
|
||||
${chalk.bold("Arguments:")}
|
||||
query Search query text
|
||||
|
||||
${chalk.bold("Options:")}
|
||||
--provider <name> Provider: ${PROVIDERS.join(", ")}
|
||||
--recency <value> Recency filter (Perplexity only): ${RECENCY_OPTIONS.join(", ")}
|
||||
-l, --limit <n> Max results to return
|
||||
--compact Render condensed output
|
||||
-h, --help Show this help
|
||||
|
||||
${chalk.bold("Examples:")}
|
||||
${APP_NAME} q --provider=exa "what's the color of the sky"
|
||||
${APP_NAME} q --provider=perplexity --recency=week "latest TypeScript 5.7 changes"
|
||||
`);
|
||||
}
|
||||
|
||||
function writeStdout(message: string): void {
|
||||
process.stdout.write(`${message}\n`);
|
||||
}
|
||||
|
||||
function writeStderr(message: string): void {
|
||||
process.stderr.write(`${message}\n`);
|
||||
}
|
||||
@@ -563,6 +563,17 @@ export async function main(args: string[]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle web search subcommand (for testing web search providers)
|
||||
const webSearchCmd = parseWebSearchArgs(args);
|
||||
if (webSearchCmd) {
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
printWebSearchHelp();
|
||||
return;
|
||||
}
|
||||
await runWebSearchCommand(webSearchCmd);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle shell subcommand (for testing brush-core shell)
|
||||
const shellCmd = parseShellArgs(args);
|
||||
if (shellCmd) {
|
||||
|
||||
@@ -24,13 +24,12 @@ import webSearchSystemPrompt from "../../prompts/system/web-search.md" with { ty
|
||||
import webSearchDescription from "../../prompts/tools/web-search.md" with { type: "text" };
|
||||
import type { ToolSession } from "../../tools";
|
||||
import { formatAge } from "../../tools/render-utils";
|
||||
import { findAnthropicAuth } from "./auth";
|
||||
import { searchAnthropic } from "./providers/anthropic";
|
||||
import { searchCodex, hasCodexWebSearch } from "./providers/codex";
|
||||
import { searchExa } from "./providers/exa";
|
||||
import { searchGemini, hasGeminiWebSearch } from "./providers/gemini";
|
||||
import { findApiKey as findJinaKey, searchJina } from "./providers/jina";
|
||||
import { findApiKey as findPerplexityKey, searchPerplexity } from "./providers/perplexity";
|
||||
import {
|
||||
formatWebSearchProviderLabel,
|
||||
getWebSearchProviderInfo,
|
||||
WEB_SEARCH_PROVIDER_ORDER,
|
||||
WEB_SEARCH_PROVIDERS,
|
||||
} from "./provider-info";
|
||||
import { renderWebSearchCall, renderWebSearchResult, type WebSearchRenderDetails } from "./render";
|
||||
import type { WebSearchProvider, WebSearchResponse } from "./types";
|
||||
import { WebSearchProviderError } from "./types";
|
||||
@@ -58,90 +57,6 @@ export type WebSearchParams = {
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
interface WebSearchProviderDefinition {
|
||||
id: WebSearchProvider;
|
||||
label: string;
|
||||
isAvailable: () => Promise<boolean>;
|
||||
search: (params: {
|
||||
query: string;
|
||||
limit?: number;
|
||||
recency?: "day" | "week" | "month" | "year";
|
||||
systemPrompt: string;
|
||||
signal?: AbortSignal;
|
||||
}) => Promise<WebSearchResponse>;
|
||||
}
|
||||
|
||||
const webSearchProviders: WebSearchProviderDefinition[] = [
|
||||
{
|
||||
id: "exa",
|
||||
label: "Exa",
|
||||
isAvailable: async () => Boolean(findExaKey()),
|
||||
search: async ({ query, limit }) =>
|
||||
searchExa({
|
||||
query,
|
||||
num_results: limit,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "jina",
|
||||
label: "Jina",
|
||||
isAvailable: async () => Boolean(findJinaKey()),
|
||||
search: async ({ query, limit }) =>
|
||||
searchJina({
|
||||
query,
|
||||
num_results: limit,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "perplexity",
|
||||
label: "Perplexity",
|
||||
isAvailable: async () => Boolean(findPerplexityKey()),
|
||||
search: async ({ query, limit, recency, systemPrompt }) =>
|
||||
searchPerplexity({
|
||||
query,
|
||||
system_prompt: systemPrompt,
|
||||
search_recency_filter: recency,
|
||||
num_results: limit,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "anthropic",
|
||||
label: "Anthropic",
|
||||
isAvailable: async () => Boolean(await findAnthropicAuth()),
|
||||
search: async ({ query, limit, systemPrompt }) =>
|
||||
searchAnthropic({
|
||||
query,
|
||||
system_prompt: systemPrompt,
|
||||
num_results: limit,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "gemini",
|
||||
label: "Gemini",
|
||||
isAvailable: hasGeminiWebSearch,
|
||||
search: async ({ query, limit, systemPrompt }) =>
|
||||
searchGemini({
|
||||
query,
|
||||
system_prompt: systemPrompt,
|
||||
num_results: limit,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
isAvailable: hasCodexWebSearch,
|
||||
search: async ({ query, limit, systemPrompt, signal }) =>
|
||||
searchCodex({
|
||||
signal,
|
||||
query,
|
||||
system_prompt: systemPrompt,
|
||||
num_results: limit,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const webSearchProviderMap = new Map(webSearchProviders.map(provider => [provider.id, provider]));
|
||||
|
||||
/** Preferred provider set via settings (default: auto) */
|
||||
let preferredProvider: WebSearchProvider | "auto" = "auto";
|
||||
|
||||
@@ -154,21 +69,18 @@ export function setPreferredWebSearchProvider(provider: WebSearchProvider | "aut
|
||||
async function getAvailableProviders(): Promise<WebSearchProvider[]> {
|
||||
const providers: WebSearchProvider[] = [];
|
||||
|
||||
for (const provider of webSearchProviders) {
|
||||
if (await provider.isAvailable()) {
|
||||
providers.push(provider.id);
|
||||
for (const provider of WEB_SEARCH_PROVIDER_ORDER) {
|
||||
const definition = getWebSearchProviderInfo(provider);
|
||||
if (await definition.isAvailable()) {
|
||||
providers.push(provider);
|
||||
}
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
function formatProviderLabel(provider: WebSearchProvider): string {
|
||||
return webSearchProviderMap.get(provider)?.label ?? provider;
|
||||
}
|
||||
|
||||
function formatProviderList(providers: WebSearchProvider[]): string {
|
||||
return providers.map(provider => formatProviderLabel(provider)).join(", ");
|
||||
return providers.map(provider => formatWebSearchProviderLabel(provider)).join(", ");
|
||||
}
|
||||
|
||||
function formatProviderError(error: unknown, provider: WebSearchProvider): string {
|
||||
@@ -177,12 +89,12 @@ function formatProviderError(error: unknown, provider: WebSearchProvider): strin
|
||||
return "Anthropic web search returned 404 (model or endpoint not found).";
|
||||
}
|
||||
if (error.status === 401 || error.status === 403) {
|
||||
return `${formatProviderLabel(error.provider)} authorization failed (${error.status}). Check API key or base URL.`;
|
||||
return `${formatWebSearchProviderLabel(error.provider)} authorization failed (${error.status}). Check API key or base URL.`;
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
if (error instanceof Error) return error.message;
|
||||
return `Unknown error from ${formatProviderLabel(provider)}`;
|
||||
return `Unknown error from ${formatWebSearchProviderLabel(provider)}`;
|
||||
}
|
||||
|
||||
async function resolveProviderChain(
|
||||
@@ -305,7 +217,7 @@ async function executeWebSearch(
|
||||
|
||||
for (const provider of providers) {
|
||||
lastProvider = provider;
|
||||
const providerDefinition = webSearchProviderMap.get(provider);
|
||||
const providerDefinition = WEB_SEARCH_PROVIDERS[provider];
|
||||
if (!providerDefinition) {
|
||||
lastError = new Error(`Unknown web search provider: ${provider}`);
|
||||
if (!allowFallback) break;
|
||||
@@ -313,7 +225,7 @@ async function executeWebSearch(
|
||||
}
|
||||
try {
|
||||
const response = await providerDefinition.search({
|
||||
query: params.query,
|
||||
query: params.query.replace(/202\d/g, String(new Date().getFullYear())), // LUL
|
||||
limit: params.limit,
|
||||
recency: params.recency,
|
||||
systemPrompt: webSearchSystemPrompt,
|
||||
@@ -343,6 +255,15 @@ async function executeWebSearch(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a web search query for CLI/testing workflows.
|
||||
*/
|
||||
export async function runWebSearchQuery(
|
||||
params: WebSearchParams,
|
||||
): Promise<{ content: Array<{ type: "text"; text: string }>; details: WebSearchRenderDetails }> {
|
||||
return executeWebSearch("cli-web-search", params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Web search tool implementation.
|
||||
*
|
||||
@@ -693,4 +614,10 @@ export async function hasExaWebSearch(): Promise<boolean> {
|
||||
return exaKey !== null;
|
||||
}
|
||||
|
||||
export {
|
||||
formatWebSearchProviderLabel,
|
||||
getWebSearchProviderInfo,
|
||||
WEB_SEARCH_PROVIDER_ORDER,
|
||||
WEB_SEARCH_PROVIDERS,
|
||||
} from "./provider-info";
|
||||
export type { WebSearchProvider, WebSearchResponse } from "./types";
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { findAnthropicAuth } from "./auth";
|
||||
import { searchAnthropic } from "./providers/anthropic";
|
||||
import { searchCodex, hasCodexWebSearch } from "./providers/codex";
|
||||
import { searchExa } from "./providers/exa";
|
||||
import { searchGemini, hasGeminiWebSearch } from "./providers/gemini";
|
||||
import { findApiKey as findJinaKey, searchJina } from "./providers/jina";
|
||||
import { findApiKey as findPerplexityKey, searchPerplexity } from "./providers/perplexity";
|
||||
import type { WebSearchProvider, WebSearchResponse } from "./types";
|
||||
import { findApiKey as findExaKey } from "../../exa/mcp-client";
|
||||
|
||||
export interface WebSearchProviderInfo {
|
||||
id: WebSearchProvider;
|
||||
label: string;
|
||||
/** Authentication requirement summary. */
|
||||
auth: string;
|
||||
/** Query hint for UI or docs. */
|
||||
query: string;
|
||||
isAvailable: () => Promise<boolean>;
|
||||
search: (params: {
|
||||
query: string;
|
||||
limit?: number;
|
||||
recency?: "day" | "week" | "month" | "year";
|
||||
systemPrompt: string;
|
||||
signal?: AbortSignal;
|
||||
}) => Promise<WebSearchResponse>;
|
||||
}
|
||||
|
||||
export const WEB_SEARCH_PROVIDER_ORDER: WebSearchProvider[] = [
|
||||
"exa",
|
||||
"jina",
|
||||
"perplexity",
|
||||
"anthropic",
|
||||
"gemini",
|
||||
"codex",
|
||||
];
|
||||
|
||||
export const WEB_SEARCH_PROVIDERS: Record<WebSearchProvider, WebSearchProviderInfo> = {
|
||||
exa: {
|
||||
id: "exa",
|
||||
label: "Exa",
|
||||
auth: "EXA_API_KEY",
|
||||
query: "Search query",
|
||||
isAvailable: async () => Boolean(findExaKey()),
|
||||
search: async ({ query, limit }) =>
|
||||
searchExa({
|
||||
query,
|
||||
num_results: limit,
|
||||
}),
|
||||
},
|
||||
jina: {
|
||||
id: "jina",
|
||||
label: "Jina",
|
||||
auth: "JINA_API_KEY",
|
||||
query: "Search query",
|
||||
isAvailable: async () => Boolean(findJinaKey()),
|
||||
search: async ({ query, limit }) =>
|
||||
searchJina({
|
||||
query,
|
||||
num_results: limit,
|
||||
}),
|
||||
},
|
||||
perplexity: {
|
||||
id: "perplexity",
|
||||
label: "Perplexity",
|
||||
auth: "PERPLEXITY_API_KEY / PPLX_API_KEY",
|
||||
query: "Search query",
|
||||
isAvailable: async () => Boolean(findPerplexityKey()),
|
||||
search: async ({ query, limit, recency, systemPrompt }) =>
|
||||
searchPerplexity({
|
||||
query,
|
||||
system_prompt: systemPrompt,
|
||||
search_recency_filter: recency,
|
||||
num_results: limit,
|
||||
}),
|
||||
},
|
||||
anthropic: {
|
||||
id: "anthropic",
|
||||
label: "Anthropic",
|
||||
auth: "ANTHROPIC_SEARCH_API_KEY / OAuth",
|
||||
query: "Search query",
|
||||
isAvailable: async () => Boolean(await findAnthropicAuth()),
|
||||
search: async ({ query, limit, systemPrompt }) =>
|
||||
searchAnthropic({
|
||||
query,
|
||||
system_prompt: systemPrompt,
|
||||
num_results: limit,
|
||||
}),
|
||||
},
|
||||
gemini: {
|
||||
id: "gemini",
|
||||
label: "Gemini",
|
||||
auth: "OAuth (agent.db: google-antigravity/google-gemini-cli)",
|
||||
query: "Search query",
|
||||
isAvailable: hasGeminiWebSearch,
|
||||
search: async ({ query, limit, systemPrompt }) =>
|
||||
searchGemini({
|
||||
query,
|
||||
system_prompt: systemPrompt,
|
||||
num_results: limit,
|
||||
}),
|
||||
},
|
||||
codex: {
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
auth: "OAuth (agent.db: openai-codex)",
|
||||
query: "Search query",
|
||||
isAvailable: hasCodexWebSearch,
|
||||
search: async ({ query, limit, systemPrompt, signal }) =>
|
||||
searchCodex({
|
||||
signal,
|
||||
query,
|
||||
system_prompt: systemPrompt,
|
||||
num_results: limit,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export function formatWebSearchProviderLabel(provider: WebSearchProvider): string {
|
||||
return WEB_SEARCH_PROVIDERS[provider]?.label ?? "Unknown";
|
||||
}
|
||||
|
||||
export function getWebSearchProviderInfo(provider: WebSearchProvider): WebSearchProviderInfo {
|
||||
return WEB_SEARCH_PROVIDERS[provider];
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
||||
import type {
|
||||
PerplexityMessageOutput,
|
||||
PerplexityRequest,
|
||||
PerplexityResponse,
|
||||
WebSearchCitation,
|
||||
@@ -20,7 +21,7 @@ const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
|
||||
export interface PerplexitySearchParams {
|
||||
query: string;
|
||||
system_prompt?: string;
|
||||
search_recency_filter?: "day" | "week" | "month" | "year";
|
||||
search_recency_filter?: "hour" | "day" | "week" | "month" | "year";
|
||||
num_results?: number;
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ async function callPerplexity(apiKey: string, request: PerplexityRequest): Promi
|
||||
}
|
||||
|
||||
/** Calculate age in seconds from ISO date string */
|
||||
function dateToAgeSeconds(dateStr: string | undefined): number | undefined {
|
||||
function dateToAgeSeconds(dateStr: string | null | undefined): number | undefined {
|
||||
if (!dateStr) return undefined;
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
@@ -64,9 +65,16 @@ function dateToAgeSeconds(dateStr: string | undefined): number | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
function messageContentToText(content: PerplexityMessageOutput["content"]): string {
|
||||
if (!content) return "";
|
||||
if (typeof content === "string") return content;
|
||||
return content.map(chunk => (chunk.type === "text" ? chunk.text : "")).join("");
|
||||
}
|
||||
|
||||
/** Parse API response into unified WebSearchResponse */
|
||||
function parseResponse(response: PerplexityResponse): WebSearchResponse {
|
||||
const answer = response.choices[0]?.message?.content ?? "";
|
||||
const messageContent = response.choices[0]?.message?.content ?? null;
|
||||
const answer = messageContentToText(messageContent);
|
||||
|
||||
// Build sources by matching citations to search_results
|
||||
const sources: WebSearchSource[] = [];
|
||||
@@ -82,7 +90,7 @@ function parseResponse(response: PerplexityResponse): WebSearchResponse {
|
||||
title: searchResult?.title ?? url,
|
||||
url,
|
||||
snippet: searchResult?.snippet,
|
||||
publishedDate: searchResult?.date,
|
||||
publishedDate: searchResult?.date ?? undefined,
|
||||
ageSeconds: dateToAgeSeconds(searchResult?.date),
|
||||
});
|
||||
citations.push({
|
||||
@@ -96,7 +104,7 @@ function parseResponse(response: PerplexityResponse): WebSearchResponse {
|
||||
title: searchResult.title ?? searchResult.url,
|
||||
url: searchResult.url,
|
||||
snippet: searchResult.snippet,
|
||||
publishedDate: searchResult.date,
|
||||
publishedDate: searchResult.date ?? undefined,
|
||||
ageSeconds: dateToAgeSeconds(searchResult.date),
|
||||
});
|
||||
}
|
||||
@@ -107,12 +115,13 @@ function parseResponse(response: PerplexityResponse): WebSearchResponse {
|
||||
answer: answer || undefined,
|
||||
sources,
|
||||
citations: citations.length > 0 ? citations : undefined,
|
||||
relatedQuestions: response.related_questions,
|
||||
usage: {
|
||||
inputTokens: response.usage.prompt_tokens,
|
||||
outputTokens: response.usage.completion_tokens,
|
||||
totalTokens: response.usage.total_tokens,
|
||||
},
|
||||
usage: response.usage
|
||||
? {
|
||||
inputTokens: response.usage.prompt_tokens,
|
||||
outputTokens: response.usage.completion_tokens,
|
||||
totalTokens: response.usage.total_tokens,
|
||||
}
|
||||
: undefined,
|
||||
model: response.model,
|
||||
requestId: response.id,
|
||||
};
|
||||
@@ -134,7 +143,6 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
|
||||
const request: PerplexityRequest = {
|
||||
model: "sonar-pro",
|
||||
messages,
|
||||
return_related_questions: false,
|
||||
web_search_options: {
|
||||
search_type: "pro",
|
||||
search_context_size: "high",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Tree-based rendering with collapsed/expanded states for web search results.
|
||||
*/
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
import { Text, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
||||
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
||||
import type { Theme } from "../../modes/theme/theme";
|
||||
import {
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
truncateToWidth,
|
||||
} from "../../tools/render-utils";
|
||||
import { renderOutputBlock, renderStatusLine, renderTreeList } from "../../tui";
|
||||
import { formatWebSearchProviderLabel } from "./provider-info";
|
||||
import type { WebSearchResponse } from "./types";
|
||||
|
||||
const MAX_COLLAPSED_ANSWER_LINES = PREVIEW_LIMITS.COLLAPSED_LINES;
|
||||
@@ -70,7 +71,12 @@ export function renderWebSearchResult(
|
||||
result: { content: Array<{ type: string; text?: string }>; details?: WebSearchRenderDetails },
|
||||
options: RenderResultOptions,
|
||||
theme: Theme,
|
||||
args?: { query?: string; provider?: string },
|
||||
args?: {
|
||||
query?: string;
|
||||
provider?: string;
|
||||
allowLongAnswer?: boolean;
|
||||
maxAnswerLines?: number;
|
||||
},
|
||||
): Component {
|
||||
const { expanded } = options;
|
||||
const details = result.details;
|
||||
@@ -90,10 +96,6 @@ export function renderWebSearchResult(
|
||||
const sourceCount = sources.length;
|
||||
const citations = Array.isArray(response.citations) ? response.citations : [];
|
||||
const citationCount = citations.length;
|
||||
const related = Array.isArray(response.relatedQuestions)
|
||||
? response.relatedQuestions.filter(item => typeof item === "string")
|
||||
: [];
|
||||
const relatedCount = related.length;
|
||||
const searchQueries = Array.isArray(response.searchQueries)
|
||||
? response.searchQueries.filter(item => typeof item === "string")
|
||||
: [];
|
||||
@@ -102,20 +104,21 @@ export function renderWebSearchResult(
|
||||
// Get answer text
|
||||
const answerText = typeof response.answer === "string" ? response.answer.trim() : "";
|
||||
const contentText = answerText || rawText;
|
||||
const totalAnswerLines = contentText ? contentText.split("\n").filter(l => l.trim()).length : 0;
|
||||
const answerLines = contentText
|
||||
? contentText
|
||||
.split("\n")
|
||||
.filter(l => l.trim())
|
||||
.map(l => l.trim())
|
||||
: [];
|
||||
const totalAnswerLines = answerLines.length;
|
||||
const answerLimit = expanded ? MAX_EXPANDED_ANSWER_LINES : MAX_COLLAPSED_ANSWER_LINES;
|
||||
const answerPreview = contentText ? getPreviewLines(contentText, answerLimit, MAX_ANSWER_LINE_LEN) : [];
|
||||
const answerPreview = contentText
|
||||
? args?.allowLongAnswer
|
||||
? answerLines.slice(0, args.maxAnswerLines ?? answerLines.length)
|
||||
: getPreviewLines(contentText, answerLimit, MAX_ANSWER_LINE_LEN)
|
||||
: [];
|
||||
|
||||
const providerLabel =
|
||||
provider === "anthropic"
|
||||
? "Anthropic"
|
||||
: provider === "perplexity"
|
||||
? "Perplexity"
|
||||
: provider === "exa"
|
||||
? "Exa"
|
||||
: provider === "jina"
|
||||
? "Jina"
|
||||
: "Unknown";
|
||||
const providerLabel = formatWebSearchProviderLabel(provider);
|
||||
const queryPreview = args?.query
|
||||
? truncateToWidth(args.query, 80)
|
||||
: searchQueries[0]
|
||||
@@ -132,20 +135,6 @@ export function renderWebSearchResult(
|
||||
);
|
||||
|
||||
const remainingAnswer = totalAnswerLines - answerPreview.length;
|
||||
const answerLines = answerPreview.length > 0 ? answerPreview : ["No answer text returned"];
|
||||
const answerTree = renderTreeList(
|
||||
{
|
||||
items: answerLines,
|
||||
expanded: true,
|
||||
maxCollapsed: answerLines.length,
|
||||
itemType: "line",
|
||||
renderItem: line => (line === "No answer text returned" ? theme.fg("muted", line) : theme.fg("dim", line)),
|
||||
},
|
||||
theme,
|
||||
);
|
||||
if (remainingAnswer > 0) {
|
||||
answerTree.push(theme.fg("muted", formatMoreItems(remainingAnswer, "line")));
|
||||
}
|
||||
|
||||
const sourceTree = renderTreeList(
|
||||
{
|
||||
@@ -185,28 +174,12 @@ export function renderWebSearchResult(
|
||||
theme,
|
||||
);
|
||||
|
||||
const relatedLines = related.length > 0 ? related : ["No related questions"];
|
||||
const relatedTree = renderTreeList(
|
||||
{
|
||||
items: relatedLines,
|
||||
expanded,
|
||||
maxCollapsed: MAX_COLLAPSED_ITEMS,
|
||||
itemType: "question",
|
||||
renderItem: line => theme.fg("muted", line === "No related questions" ? line : `${theme.format.dash} ${line}`),
|
||||
},
|
||||
theme,
|
||||
);
|
||||
if (!expanded && relatedCount > MAX_COLLAPSED_ITEMS) {
|
||||
relatedTree.push(theme.fg("muted", formatMoreItems(relatedCount - MAX_COLLAPSED_ITEMS, "question")));
|
||||
}
|
||||
|
||||
const metaLines: string[] = [];
|
||||
metaLines.push(`${theme.fg("muted", "Provider:")} ${theme.fg("text", providerLabel)}`);
|
||||
if (response.model) metaLines.push(`${theme.fg("muted", "Model:")} ${theme.fg("text", response.model)}`);
|
||||
metaLines.push(`${theme.fg("muted", "Sources:")} ${theme.fg("text", String(sourceCount))}`);
|
||||
if (citationCount > 0)
|
||||
metaLines.push(`${theme.fg("muted", "Citations:")} ${theme.fg("text", String(citationCount))}`);
|
||||
if (relatedCount > 0) metaLines.push(`${theme.fg("muted", "Related:")} ${theme.fg("text", String(relatedCount))}`);
|
||||
if (response.usage) {
|
||||
const usageParts: string[] = [];
|
||||
if (response.usage.inputTokens !== undefined) usageParts.push(`in ${response.usage.inputTokens}`);
|
||||
@@ -228,30 +201,66 @@ export function renderWebSearchResult(
|
||||
metaLines.push(`${theme.fg("muted", "Queries:")} ${theme.fg("text", queryList.join("; "))}${suffix}`);
|
||||
}
|
||||
|
||||
const sections = [
|
||||
...(queryPreview
|
||||
? [
|
||||
{
|
||||
lines: [`${theme.fg("muted", "Query:")} ${theme.fg("text", queryPreview)}`],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: theme.fg("toolTitle", "Answer"), lines: answerTree },
|
||||
{
|
||||
label: theme.fg("toolTitle", "Sources"),
|
||||
lines: sourceTree.length > 0 ? sourceTree : [theme.fg("muted", "No sources returned")],
|
||||
},
|
||||
{ label: theme.fg("toolTitle", "Related"), lines: relatedTree },
|
||||
{ label: theme.fg("toolTitle", "Metadata"), lines: metaLines },
|
||||
];
|
||||
|
||||
return {
|
||||
render: (width: number) =>
|
||||
renderOutputBlock(
|
||||
{
|
||||
header,
|
||||
state: sourceCount > 0 ? "success" : "warning",
|
||||
sections,
|
||||
sections: [
|
||||
...(queryPreview
|
||||
? [
|
||||
{
|
||||
lines: [`${theme.fg("muted", "Query:")} ${theme.fg("text", queryPreview)}`],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: theme.fg("toolTitle", "Answer"),
|
||||
lines: (() => {
|
||||
const state = sourceCount > 0 ? "success" : "warning";
|
||||
const borderColor: "warning" | "dim" = state === "warning" ? "warning" : "dim";
|
||||
const border = (text: string) => theme.fg(borderColor, text);
|
||||
const contentPrefix = border(`${theme.boxSharp.vertical} `);
|
||||
const contentSuffix = border(theme.boxSharp.vertical);
|
||||
const contentWidth = Math.max(
|
||||
0,
|
||||
width - visibleWidth(contentPrefix) - visibleWidth(contentSuffix),
|
||||
);
|
||||
const answerTreeLines = answerPreview.length > 0 ? answerPreview : ["No answer text returned"];
|
||||
const answerTree = renderTreeList(
|
||||
{
|
||||
items: answerTreeLines,
|
||||
expanded: true,
|
||||
maxCollapsed: answerTreeLines.length,
|
||||
itemType: "line",
|
||||
renderItem: (line, context) => {
|
||||
const coloredLine =
|
||||
line === "No answer text returned"
|
||||
? theme.fg("muted", line)
|
||||
: theme.fg("dim", line);
|
||||
if (!args?.allowLongAnswer) {
|
||||
return coloredLine;
|
||||
}
|
||||
const prefixWidth = visibleWidth(context.continuePrefix);
|
||||
const wrapWidth = Math.max(10, contentWidth - prefixWidth);
|
||||
return wrapTextWithAnsi(coloredLine, wrapWidth);
|
||||
},
|
||||
},
|
||||
theme,
|
||||
);
|
||||
if (remainingAnswer > 0) {
|
||||
answerTree.push(theme.fg("muted", formatMoreItems(remainingAnswer, "line")));
|
||||
}
|
||||
return answerTree;
|
||||
})(),
|
||||
},
|
||||
{
|
||||
label: theme.fg("toolTitle", "Sources"),
|
||||
lines: sourceTree.length > 0 ? sourceTree : [theme.fg("muted", "No sources returned")],
|
||||
},
|
||||
{ label: theme.fg("toolTitle", "Metadata"), lines: metaLines },
|
||||
],
|
||||
width,
|
||||
},
|
||||
theme,
|
||||
|
||||
@@ -45,8 +45,6 @@ export interface WebSearchResponse {
|
||||
sources: WebSearchSource[];
|
||||
/** Text citations with context */
|
||||
citations?: WebSearchCitation[];
|
||||
/** Follow-up questions (perplexity) */
|
||||
relatedQuestions?: string[];
|
||||
/** Intermediate search queries (anthropic) */
|
||||
searchQueries?: string[];
|
||||
/** Token usage metrics */
|
||||
@@ -148,52 +146,283 @@ export interface AnthropicApiResponse {
|
||||
}
|
||||
|
||||
/** Perplexity API types */
|
||||
export interface PerplexityMessage {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
export type PerplexityChatMessageRole = "system" | "user" | "assistant" | "tool";
|
||||
|
||||
export interface PerplexityUrl {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface PerplexityVideoUrl {
|
||||
url: string;
|
||||
frame_interval?: string | number;
|
||||
}
|
||||
|
||||
export interface PerplexityContentTextChunk {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface PerplexityContentImageChunk {
|
||||
type: "image_url";
|
||||
image_url: PerplexityUrl | string;
|
||||
}
|
||||
|
||||
export interface PerplexityContentFileChunk {
|
||||
type: "file_url";
|
||||
file_url: PerplexityUrl | string;
|
||||
file_name?: string | null;
|
||||
}
|
||||
|
||||
export interface PerplexityContentPdfChunk {
|
||||
type: "pdf_url";
|
||||
pdf_url: PerplexityUrl | string;
|
||||
}
|
||||
|
||||
export interface PerplexityContentVideoChunk {
|
||||
type: "video_url";
|
||||
video_url: PerplexityVideoUrl | string;
|
||||
}
|
||||
|
||||
export type PerplexityContentChunk =
|
||||
| PerplexityContentTextChunk
|
||||
| PerplexityContentImageChunk
|
||||
| PerplexityContentFileChunk
|
||||
| PerplexityContentPdfChunk
|
||||
| PerplexityContentVideoChunk;
|
||||
|
||||
export interface PerplexityWebSearchStepDetails {
|
||||
search_results: PerplexitySearchResult[];
|
||||
search_keywords: string[];
|
||||
}
|
||||
|
||||
export interface PerplexityFetchUrlContentStepDetails {
|
||||
contents: PerplexitySearchResult[];
|
||||
}
|
||||
|
||||
export interface PerplexityExecutePythonStepDetails {
|
||||
code: string;
|
||||
result: string;
|
||||
}
|
||||
|
||||
export interface PerplexityReasoningStepInput {
|
||||
thought: string;
|
||||
type?: string | null;
|
||||
web_search?: PerplexityWebSearchStepDetails | null;
|
||||
fetch_url_content?: PerplexityFetchUrlContentStepDetails | null;
|
||||
execute_python?: PerplexityExecutePythonStepDetails | null;
|
||||
}
|
||||
|
||||
export interface PerplexityReasoningStepOutput {
|
||||
thought: string;
|
||||
type?: string | null;
|
||||
web_search?: PerplexityWebSearchStepDetails | null;
|
||||
fetch_url_content?: PerplexityFetchUrlContentStepDetails | null;
|
||||
execute_python?: PerplexityExecutePythonStepDetails | null;
|
||||
}
|
||||
|
||||
export interface PerplexityToolCallFunction {
|
||||
name?: string | null;
|
||||
arguments?: string | null;
|
||||
}
|
||||
|
||||
export interface PerplexityToolCall {
|
||||
id?: string | null;
|
||||
type?: "function" | null;
|
||||
function?: PerplexityToolCallFunction | null;
|
||||
}
|
||||
|
||||
export interface PerplexityMessageInput {
|
||||
role: PerplexityChatMessageRole;
|
||||
content: string | PerplexityContentChunk[] | null;
|
||||
reasoning_steps?: PerplexityReasoningStepInput[] | null;
|
||||
tool_calls?: PerplexityToolCall[] | null;
|
||||
tool_call_id?: string | null;
|
||||
}
|
||||
|
||||
export interface PerplexityMessageOutput {
|
||||
role: PerplexityChatMessageRole;
|
||||
content: string | PerplexityContentChunk[] | null;
|
||||
reasoning_steps?: PerplexityReasoningStepOutput[] | null;
|
||||
tool_calls?: PerplexityToolCall[] | null;
|
||||
tool_call_id?: string | null;
|
||||
}
|
||||
|
||||
export type PerplexityMessage = PerplexityMessageInput;
|
||||
|
||||
export interface PerplexityResponseFormatText {
|
||||
type: "text";
|
||||
}
|
||||
|
||||
export interface PerplexityJSONSchema {
|
||||
schema: Record<string, unknown>;
|
||||
name?: string | null;
|
||||
description?: string | null;
|
||||
strict?: boolean | null;
|
||||
}
|
||||
|
||||
export interface PerplexityResponseFormatJSONSchema {
|
||||
type: "json_schema";
|
||||
json_schema: PerplexityJSONSchema;
|
||||
}
|
||||
|
||||
export interface PerplexityRegexSchema {
|
||||
regex: string;
|
||||
name?: string | null;
|
||||
description?: string | null;
|
||||
strict?: boolean | null;
|
||||
}
|
||||
|
||||
export interface PerplexityResponseFormatRegex {
|
||||
type: "regex";
|
||||
regex: PerplexityRegexSchema;
|
||||
}
|
||||
|
||||
export type PerplexityResponseFormat =
|
||||
| PerplexityResponseFormatText
|
||||
| PerplexityResponseFormatJSONSchema
|
||||
| PerplexityResponseFormatRegex;
|
||||
|
||||
export interface PerplexityParameterSpec {
|
||||
type: string;
|
||||
properties: Record<string, unknown>;
|
||||
required?: string[] | null;
|
||||
additional_properties?: boolean | null;
|
||||
}
|
||||
|
||||
export interface PerplexityFunctionSpec {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: PerplexityParameterSpec;
|
||||
strict?: boolean | null;
|
||||
}
|
||||
|
||||
export interface PerplexityToolSpec {
|
||||
type: "function";
|
||||
function: PerplexityFunctionSpec;
|
||||
}
|
||||
|
||||
export interface PerplexityUserLocation {
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
country?: string | null;
|
||||
city?: string | null;
|
||||
region?: string | null;
|
||||
}
|
||||
|
||||
export interface PerplexityWebSearchOptions {
|
||||
search_context_size?: "low" | "medium" | "high";
|
||||
search_type?: "fast" | "pro" | "auto" | null;
|
||||
user_location?: PerplexityUserLocation | null;
|
||||
image_results_enhanced_relevance?: boolean;
|
||||
}
|
||||
|
||||
export interface PerplexityRequest {
|
||||
max_tokens?: number | null;
|
||||
n?: number | null;
|
||||
model: string;
|
||||
messages: PerplexityMessage[];
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
search_domain_filter?: string[];
|
||||
search_recency_filter?: "day" | "week" | "month" | "year";
|
||||
return_images?: boolean;
|
||||
return_related_questions?: boolean;
|
||||
web_search_options?: {
|
||||
search_type?: "fast" | "pro";
|
||||
search_context_size?: "low" | "medium" | "high";
|
||||
};
|
||||
stream?: boolean | null;
|
||||
stop?: string | string[] | null;
|
||||
cum_logprobs?: boolean | null;
|
||||
logprobs?: boolean | null;
|
||||
top_logprobs?: number | null;
|
||||
best_of?: number | null;
|
||||
response_metadata?: Record<string, unknown> | null;
|
||||
response_format?: PerplexityResponseFormat | null;
|
||||
diverse_first_token?: boolean | null;
|
||||
_inputs?: number[] | null;
|
||||
_prompt_token_length?: number | null;
|
||||
messages: PerplexityMessageInput[];
|
||||
tools?: PerplexityToolSpec[] | null;
|
||||
tool_choice?: "none" | "auto" | "required" | null;
|
||||
parallel_tool_calls?: boolean | null;
|
||||
web_search_options?: PerplexityWebSearchOptions;
|
||||
search_mode?: "web" | "academic" | "sec" | null;
|
||||
return_images?: boolean | null;
|
||||
return_related_questions?: boolean | null;
|
||||
num_search_results?: number;
|
||||
num_images?: number;
|
||||
enable_search_classifier?: boolean | null;
|
||||
disable_search?: boolean | null;
|
||||
search_domain_filter?: string[] | null;
|
||||
search_language_filter?: string[] | null;
|
||||
search_tenant?: string | null;
|
||||
ranking_model?: string | null;
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
country?: string | null;
|
||||
search_recency_filter?: "hour" | "day" | "week" | "month" | "year" | null;
|
||||
search_after_date_filter?: string | null;
|
||||
search_before_date_filter?: string | null;
|
||||
last_updated_before_filter?: string | null;
|
||||
last_updated_after_filter?: string | null;
|
||||
image_format_filter?: string[] | null;
|
||||
image_domain_filter?: string[] | null;
|
||||
safe_search?: boolean | null;
|
||||
file_workspace_id?: string | null;
|
||||
updated_before_timestamp?: number | null;
|
||||
updated_after_timestamp?: number | null;
|
||||
search_internal_properties?: Record<string, unknown> | null;
|
||||
use_threads?: boolean | null;
|
||||
thread_id?: string | null;
|
||||
stream_mode?: "full" | "concise";
|
||||
_debug_pro_search?: boolean;
|
||||
has_image_url?: boolean;
|
||||
reasoning_effort?: "minimal" | "low" | "medium" | "high" | null;
|
||||
language_preference?: string | null;
|
||||
user_original_query?: string | null;
|
||||
_force_new_agent?: boolean | null;
|
||||
}
|
||||
|
||||
export interface PerplexitySearchResult {
|
||||
title: string;
|
||||
url: string;
|
||||
date?: string;
|
||||
date?: string | null;
|
||||
last_updated?: string | null;
|
||||
snippet?: string;
|
||||
source?: "web" | "attachment";
|
||||
}
|
||||
|
||||
export interface PerplexityCost {
|
||||
input_tokens_cost: number;
|
||||
output_tokens_cost: number;
|
||||
reasoning_tokens_cost?: number | null;
|
||||
request_cost?: number | null;
|
||||
citation_tokens_cost?: number | null;
|
||||
search_queries_cost?: number | null;
|
||||
total_cost: number;
|
||||
}
|
||||
|
||||
export interface PerplexityUsageInfo {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
search_context_size?: string | null;
|
||||
citation_tokens?: number | null;
|
||||
num_search_queries?: number | null;
|
||||
reasoning_tokens?: number | null;
|
||||
cost: PerplexityCost;
|
||||
}
|
||||
|
||||
export type PerplexityCompletionResponseType = "message" | "info" | "end_of_stream";
|
||||
|
||||
export type PerplexityCompletionResponseStatus = "PENDING" | "COMPLETED";
|
||||
|
||||
export interface PerplexityChoice {
|
||||
index: number;
|
||||
finish_reason?: "stop" | "length" | null;
|
||||
message: PerplexityMessageOutput;
|
||||
delta: PerplexityMessageOutput;
|
||||
}
|
||||
|
||||
export interface PerplexityResponse {
|
||||
id: string;
|
||||
model: string;
|
||||
created: number;
|
||||
usage: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
search_context_size?: string;
|
||||
};
|
||||
citations?: string[];
|
||||
search_results?: PerplexitySearchResult[];
|
||||
related_questions?: string[];
|
||||
choices: Array<{
|
||||
index: number;
|
||||
finish_reason: string;
|
||||
message: {
|
||||
role: string;
|
||||
content: string;
|
||||
};
|
||||
}>;
|
||||
usage?: PerplexityUsageInfo | null;
|
||||
object?: string;
|
||||
choices: PerplexityChoice[];
|
||||
citations?: string[] | null;
|
||||
search_results?: PerplexitySearchResult[] | null;
|
||||
type?: PerplexityCompletionResponseType | null;
|
||||
status?: PerplexityCompletionResponseStatus | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user