feat: add Perplexity Search integration#516
Conversation
Add @tanstack/ai-perplexity with: - Search API tool (POST https://api.perplexity.ai/search) wired as a TanStack AI tool definition. Returns {title, url, snippet, date?} per result and surfaces max_results, search_domain_filter, search_recency_filter, and date filters. - OpenAI-compatible chat client factory pointed at https://api.perplexity.ai so existing openai-SDK code can target Perplexity by swapping baseURL. - API key resolution from PERPLEXITY_API_KEY (falls back to PPLX_API_KEY). - Tests with mocked fetch covering auth, body shape, filter pass-through, domain-allow/deny mixing guard, error surface, and env-var fallback. - README + docs/adapters/perplexity.md page wired into docs/config.json.
📝 WalkthroughWalkthroughThis pull request introduces Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (2)
packages/typescript/ai-perplexity/tests/search-client.test.ts (1)
105-111: ⚡ Quick winConsider adding boundary tests for stricter input validation paths.
If you adopt stricter validation in
PerplexitySearchClient.search, add tests for whitespace-onlyqueryand out-of-rangemax_resultsto prevent regressions.Suggested test additions
+ it('throws when query is whitespace-only', async () => { + const client = new PerplexitySearchClient({ fetch: vi.fn() as any, apiKey: 'k' }) + await expect(client.search({ query: ' ' })).rejects.toThrow(/non-empty `query`/i) + }) + + it('throws when max_results is out of range', async () => { + const client = new PerplexitySearchClient({ fetch: vi.fn() as any, apiKey: 'k' }) + await expect(client.search({ query: 'q', max_results: 21 })).rejects.toThrow( + /max_results/i, + ) + })Also applies to: 66-90
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/typescript/ai-perplexity/tests/search-client.test.ts` around lines 105 - 111, Add boundary tests to cover stricter validation in PerplexitySearchClient.search: add one test that calls client.search with a whitespace-only query (e.g., " ") and expects it to reject with the same non-empty `query` error, and add tests that pass invalid `max_results` values (e.g., 0 and a value > allowed max) and expect rejects with the out-of-range validation error; reference the PerplexitySearchClient class and its search method when locating where to extend tests so future tightening of validation won't regress.packages/typescript/ai-perplexity/tests/chat-client.test.ts (1)
22-26: ⚡ Quick winAdd a regression test for blank explicit
apiKey.Current coverage checks explicit non-empty keys, but not
''/whitespace keys. Adding that case will lock in expected behavior after key normalization.Suggested test case
+ it('rejects blank explicit apiKey values', () => { + expect(() => + createPerplexityChatClient({ apiKey: ' ' }), + ).toThrow(/PERPLEXITY_API_KEY/) + })Also applies to: 42-46
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/typescript/ai-perplexity/tests/chat-client.test.ts` around lines 22 - 26, Add regression tests that assert blank explicit apiKey values are normalized and do not fall back to env: extend the existing test block that uses createPerplexityChatClient and the other similar test around the second case to include checks for apiKey='' and apiKey=' ' (or other whitespace) verifying client.apiKey is treated as blank/normalized per spec; update/insert tests named e.g. "uses an explicit apiKey over the env var" and the analogous second test to cover these blank/whitespace cases so future changes to createPerplexityChatClient's normalization logic are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/typescript/ai-perplexity/package.json`:
- Line 59: The peerDependency entry for "@tanstack/ai" in the package.json uses
"workspace:^" which violates the repo guideline; change that value to
"workspace:*" in the peerDependencies block of the ai-perplexity package.json
(update the "@tanstack/ai" entry), and mirror the same replacement for any other
provider adapter packages that currently use "workspace:^" so all internal peer
deps use "workspace:*".
In `@packages/typescript/ai-perplexity/src/chat/client.ts`:
- Around line 36-40: The config destructuring passes an empty string as apiKey
into the OpenAI constructor because the current `apiKey ??
getPerplexityApiKeyFromEnv()` treats '' as provided; fix by normalizing `apiKey`
first (e.g., compute an `effectiveApiKey` using `apiKey` trimmed and treated as
missing when empty, falling back to `getPerplexityApiKeyFromEnv()`), validate
that `effectiveApiKey` is non-empty (throw or log/exit early if still missing),
and then pass `effectiveApiKey` into the `new OpenAI({...})` call instead of the
raw `apiKey`.
In `@packages/typescript/ai-perplexity/src/search/client.ts`:
- Around line 69-77: PerplexitySearchClient.search currently accepts
whitespace-only queries and forwards max_results without enforcing the
documented 1–20 bounds; update the input validation in the search implementation
to trim request.query and reject it if the trimmed string is empty (throwing a
clear Error), and validate request.max_results (when defined) to be an integer
between 1 and 20 inclusive (throwing an Error if out of range) before assigning
to the body; keep existing validateDomainFilter usage and only set
body.max_results after this new validation.
- Around line 59-61: The constructor currently accepts blank strings for
config.apiKey which bypasses getPerplexityApiKeyFromEnv and causes opaque auth
errors; in the PerplexitySearchClient constructor validate config.apiKey: if
config.apiKey is provided but after trimming is empty, throw a clear Error (or
reject) stating the apiKey is invalid; otherwise use the trimmed config.apiKey
or fallback to getPerplexityApiKeyFromEnv() to set this.apiKey. Reference the
constructor, PerplexitySearchClientConfig, apiKey, and
getPerplexityApiKeyFromEnv when making the change.
- Around line 12-33: Add Zod runtime schemas for the request and response and
use them to validate wire data: create a PerplexitySearchRequestSchema (matching
the PerplexitySearchRequest interface) and a PerplexitySearchResponseSchema,
export their inferred types, and use PerplexitySearchRequestSchema.parse() to
validate the payload before sending in the client functions that accept
PerplexitySearchRequest; replace the current ad-hoc/runtime type assertions used
in the response parsing block (the response handling around the previous
"response parsing with runtime type assertions" area) with
PerplexitySearchResponseSchema.parse() to safely parse/throw on invalid
responses; ensure schemas cover optional fields (max_results,
max_tokens_per_page, search_domain_filter, search_recency_filter,
search_after_date_filter, search_before_date_filter) and include any nested
shapes asserted earlier, and update any tool definitions to reference the new
schemas.
In `@packages/typescript/ai-perplexity/src/search/tool.ts`:
- Around line 31-36: The destructured defaultMaxResults from config must be
validated before being used as a fallback: ensure defaultMaxResults is a finite
integer and within the model/schema allowed range (e.g., >= min and <= max
provided by the model or a safe capped constant); if it is missing or
out-of-range (0, negative, or too large) ignore it and fall back to the
model-provided limits or a safe cap. Add this validation where defaultMaxResults
is extracted (the const { defaultMaxResults, ...clientConfig } = config) and
again before the other usage site referenced in the review (the place that
builds the Search API request around line ~121), so the request never sends an
invalid maxResults value. Use clear checks (Number.isInteger, bounds) and a
single source of truth for the allowed min/max to apply consistently.
In `@packages/typescript/ai-perplexity/src/utils/api-key.ts`:
- Around line 15-23: The current API key selection returns whitespace-only
values (const key = env?.PERPLEXITY_API_KEY || env?.PPLX_API_KEY) which should
be rejected; update the logic in the api-key util so you trim the chosen value,
treat empty/whitespace-only strings as missing (e.g., if (!trimmedKey) throw the
existing Error), and return the trimmedKey instead of the raw value—apply this
change to the function that reads/returns the key in
packages/typescript/ai-perplexity/src/utils/api-key.ts.
In `@packages/typescript/ai-perplexity/tests/search-tool.test.ts`:
- Around line 5-11: The test mutates process.env.PERPLEXITY_API_KEY but doesn't
restore it; modify the beforeEach/afterEach in search-tool.test.ts to save the
original value into a local variable (e.g., let prevPerplexityKey) in beforeEach
and then restore it in afterEach (set process.env.PERPLEXITY_API_KEY back to
prevPerplexityKey or delete it if undefined) while keeping vi.restoreAllMocks()
intact.
---
Nitpick comments:
In `@packages/typescript/ai-perplexity/tests/chat-client.test.ts`:
- Around line 22-26: Add regression tests that assert blank explicit apiKey
values are normalized and do not fall back to env: extend the existing test
block that uses createPerplexityChatClient and the other similar test around the
second case to include checks for apiKey='' and apiKey=' ' (or other
whitespace) verifying client.apiKey is treated as blank/normalized per spec;
update/insert tests named e.g. "uses an explicit apiKey over the env var" and
the analogous second test to cover these blank/whitespace cases so future
changes to createPerplexityChatClient's normalization logic are caught.
In `@packages/typescript/ai-perplexity/tests/search-client.test.ts`:
- Around line 105-111: Add boundary tests to cover stricter validation in
PerplexitySearchClient.search: add one test that calls client.search with a
whitespace-only query (e.g., " ") and expects it to reject with the same
non-empty `query` error, and add tests that pass invalid `max_results` values
(e.g., 0 and a value > allowed max) and expect rejects with the out-of-range
validation error; reference the PerplexitySearchClient class and its search
method when locating where to extend tests so future tightening of validation
won't regress.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5ea96be4-75fd-4fab-ab1e-2d8c75325e1c
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (17)
docs/adapters/perplexity.mddocs/config.jsonpackages/typescript/ai-perplexity/README.mdpackages/typescript/ai-perplexity/package.jsonpackages/typescript/ai-perplexity/src/chat/client.tspackages/typescript/ai-perplexity/src/chat/index.tspackages/typescript/ai-perplexity/src/index.tspackages/typescript/ai-perplexity/src/search/client.tspackages/typescript/ai-perplexity/src/search/index.tspackages/typescript/ai-perplexity/src/search/tool.tspackages/typescript/ai-perplexity/src/utils/api-key.tspackages/typescript/ai-perplexity/src/utils/index.tspackages/typescript/ai-perplexity/tests/chat-client.test.tspackages/typescript/ai-perplexity/tests/search-client.test.tspackages/typescript/ai-perplexity/tests/search-tool.test.tspackages/typescript/ai-perplexity/tsconfig.jsonpackages/typescript/ai-perplexity/vite.config.ts
| "vite": "^7.2.7" | ||
| }, | ||
| "peerDependencies": { | ||
| "@tanstack/ai": "workspace:^" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify internal workspace protocol usage in package manifests
rg -n --glob 'packages/**/package.json' '"@tanstack/ai"\s*:\s*"workspace:[^*"]+"'Repository: TanStack/ai
Length of output: 1107
🏁 Script executed:
#!/bin/bash
# Check if workspace:* (without caret) is used anywhere in package.json files
rg -n --glob 'packages/**/package.json' '"@tanstack/ai"\s*:\s*"workspace:\*"'Repository: TanStack/ai
Length of output: 2156
🏁 Script executed:
#!/bin/bash
# Also check the devDependencies in the file under review
cat -n packages/typescript/ai-perplexity/package.json | grep -A 10 '"devDependencies"'Repository: TanStack/ai
Length of output: 303
🏁 Script executed:
#!/bin/bash
# Check what pattern is used in devDependencies for `@tanstack/ai` across all provider adapters
rg -n --glob 'packages/typescript/ai-*/package.json' '"@tanstack/ai"' -A 1 -B 1 | head -50Repository: TanStack/ai
Length of output: 2974
Use workspace:* for the internal peer dependency at line 59.
The peerDependency uses workspace:^ which violates the repo guideline requiring workspace:* for internal package dependencies. This is a systematic issue across provider adapter packages (ai-react, ai-vue, ai-solid, ai-svelte, ai-preact, and others use the same pattern), but ai-openrouter correctly uses workspace:* in peerDependencies, confirming this pattern is achievable.
Suggested fix
"peerDependencies": {
- "@tanstack/ai": "workspace:^"
+ "@tanstack/ai": "workspace:*"
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "@tanstack/ai": "workspace:^" | |
| "peerDependencies": { | |
| "@tanstack/ai": "workspace:*" | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai-perplexity/package.json` at line 59, The
peerDependency entry for "@tanstack/ai" in the package.json uses "workspace:^"
which violates the repo guideline; change that value to "workspace:*" in the
peerDependencies block of the ai-perplexity package.json (update the
"@tanstack/ai" entry), and mirror the same replacement for any other provider
adapter packages that currently use "workspace:^" so all internal peer deps use
"workspace:*".
| const { apiKey, baseURL, ...rest } = config | ||
| return new OpenAI({ | ||
| ...rest, | ||
| apiKey: apiKey ?? getPerplexityApiKeyFromEnv(), | ||
| baseURL: baseURL ?? DEFAULT_BASE_URL, |
There was a problem hiding this comment.
Normalize explicit apiKey input before constructing the client.
On Line 39, ?? treats '' as provided, so an empty key is sent instead of falling back or failing early.
Suggested fix
export function createPerplexityChatClient(
config: PerplexityChatClientConfig = {},
): OpenAI {
const { apiKey, baseURL, ...rest } = config
+ const resolvedApiKey =
+ typeof apiKey === 'string' && apiKey.trim().length > 0
+ ? apiKey.trim()
+ : getPerplexityApiKeyFromEnv()
return new OpenAI({
...rest,
- apiKey: apiKey ?? getPerplexityApiKeyFromEnv(),
+ apiKey: resolvedApiKey,
baseURL: baseURL ?? DEFAULT_BASE_URL,
})
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { apiKey, baseURL, ...rest } = config | |
| return new OpenAI({ | |
| ...rest, | |
| apiKey: apiKey ?? getPerplexityApiKeyFromEnv(), | |
| baseURL: baseURL ?? DEFAULT_BASE_URL, | |
| export function createPerplexityChatClient( | |
| config: PerplexityChatClientConfig = {}, | |
| ): OpenAI { | |
| const { apiKey, baseURL, ...rest } = config | |
| const resolvedApiKey = | |
| typeof apiKey === 'string' && apiKey.trim().length > 0 | |
| ? apiKey.trim() | |
| : getPerplexityApiKeyFromEnv() | |
| return new OpenAI({ | |
| ...rest, | |
| apiKey: resolvedApiKey, | |
| baseURL: baseURL ?? DEFAULT_BASE_URL, | |
| }) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai-perplexity/src/chat/client.ts` around lines 36 - 40,
The config destructuring passes an empty string as apiKey into the OpenAI
constructor because the current `apiKey ?? getPerplexityApiKeyFromEnv()` treats
'' as provided; fix by normalizing `apiKey` first (e.g., compute an
`effectiveApiKey` using `apiKey` trimmed and treated as missing when empty,
falling back to `getPerplexityApiKeyFromEnv()`), validate that `effectiveApiKey`
is non-empty (throw or log/exit early if still missing), and then pass
`effectiveApiKey` into the `new OpenAI({...})` call instead of the raw `apiKey`.
| export interface PerplexitySearchRequest { | ||
| /** The search query. */ | ||
| query: string | ||
| /** Maximum number of results to return (1–20). Defaults to the API default (10). */ | ||
| max_results?: number | ||
| /** Maximum tokens of content to return per page. */ | ||
| max_tokens_per_page?: number | ||
| /** | ||
| * Restrict (or exclude) results by domain. | ||
| * | ||
| * Use bare hostnames to allowlist (`["nytimes.com"]`) or `-` prefixed entries | ||
| * to denylist (`["-pinterest.com"]`). Allow and deny entries must NOT be | ||
| * mixed in the same request. | ||
| */ | ||
| search_domain_filter?: Array<string> | ||
| /** Restrict results by recency: `hour | day | week | month | year`. */ | ||
| search_recency_filter?: 'hour' | 'day' | 'week' | 'month' | 'year' | ||
| /** Only include results published on or after this date (m/d/yyyy). */ | ||
| search_after_date_filter?: string | ||
| /** Only include results published on or before this date (m/d/yyyy). */ | ||
| search_before_date_filter?: string | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Read-only check: verify whether zod schemas are currently used in ai-perplexity source.
rg -n --type=ts "from 'zod'|from \"zod\"|z\\.object\\(" packages/typescript/ai-perplexity/srcRepository: TanStack/ai
Length of output: 37
🏁 Script executed:
# Get the full file to see lines 12-33 and 107-118
head -n 130 packages/typescript/ai-perplexity/src/search/client.ts | tail -n +1Repository: TanStack/ai
Length of output: 4496
🏁 Script executed:
# Check if Zod is used in other packages/typescript packages for comparison
rg -l "from 'zod'|from \"zod\"" packages/typescript --type=ts | head -20Repository: TanStack/ai
Length of output: 1301
🏁 Script executed:
# Check the overall structure of ai-perplexity package
ls -la packages/typescript/ai-perplexity/src/Repository: TanStack/ai
Length of output: 383
🏁 Script executed:
# Check if other packages in packages/typescript/ai-* also use Zod
rg "from 'zod'|from \"zod\"" packages/typescript/ai-*/src --type=ts | head -10Repository: TanStack/ai
Length of output: 329
🏁 Script executed:
# Check what files are in the ai-perplexity package
find packages/typescript/ai-perplexity -name "*.ts" -type f | grep -v node_modulesRepository: TanStack/ai
Length of output: 708
🏁 Script executed:
# Check the main entry point of ai-perplexity to understand its scope
cat packages/typescript/ai-perplexity/src/index.tsRepository: TanStack/ai
Length of output: 517
🏁 Script executed:
# Check the search/tool.ts to see if it uses Zod
cat packages/typescript/ai-perplexity/src/search/tool.tsRepository: TanStack/ai
Length of output: 4473
🏁 Script executed:
# Check if other low-level clients in packages/typescript use Zod for validation
rg -A 5 "interface.*Request\|interface.*Response" packages/typescript/ai-openai/src --type=ts | head -40Repository: TanStack/ai
Length of output: 37
🏁 Script executed:
# Check the chat client as well since the comment mentions "Also applies to: 107-118"
sed -n '107,118p' packages/typescript/ai-perplexity/src/search/client.tsRepository: TanStack/ai
Length of output: 403
🏁 Script executed:
# Check how ai-openai (another API client) handles request/response validation
cat packages/typescript/ai-openai/src/create-openai-client.ts | head -100Repository: TanStack/ai
Length of output: 145
🏁 Script executed:
# Check if the main ai package uses Zod for toolDefinition
grep -A 20 "export.*toolDefinition" packages/typescript/ai/src/activities/chat/tools/tool-definition.ts | head -30Repository: TanStack/ai
Length of output: 734
🏁 Script executed:
# Look for any chat client in ai-perplexity to see the "107-118" reference
wc -l packages/typescript/ai-perplexity/src/chat/client.tsRepository: TanStack/ai
Length of output: 111
🏁 Script executed:
# Check the length of search/client.ts
wc -l packages/typescript/ai-perplexity/src/search/client.tsRepository: TanStack/ai
Length of output: 114
🏁 Script executed:
# Let's see what's around lines 107-118 in search/client.ts
sed -n '100,125p' packages/typescript/ai-perplexity/src/search/client.tsRepository: TanStack/ai
Length of output: 749
🏁 Script executed:
# Check how other packages in packages/typescript handle client configs and validation
ls -la packages/typescript/ai-openai/src/ | grep -i clientRepository: TanStack/ai
Length of output: 37
🏁 Script executed:
# Check the pattern in ai-code-mode to understand how Zod is used for tool definitions
cat packages/typescript/ai-code-mode/src/create-code-mode-tool.ts | head -80Repository: TanStack/ai
Length of output: 2292
🏁 Script executed:
# Check if there's a pattern in how other providers define their schemas
cat packages/typescript/ai-gemini/src/tools/tool-definition.ts 2>/dev/null || echo "File not found"Repository: TanStack/ai
Length of output: 70
🏁 Script executed:
# Verify the full validateDomainFilter function to understand the scope
sed -n '119,144p' packages/typescript/ai-perplexity/src/search/client.tsRepository: TanStack/ai
Length of output: 747
Add Zod schemas for request and response validation in this client.
This source currently relies on TypeScript types only, but wire payloads are runtime data. Add Zod schemas for both request validation and response parsing to align runtime guarantees with the public interfaces.
Per the coding guidelines, Zod should be used for schema validation and tool definition across the library (packages/typescript/**/src/**/*.ts).
Applies to:
- Lines 12–33:
PerplexitySearchRequestinterface - Lines 107–118: Response parsing with runtime type assertions
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai-perplexity/src/search/client.ts` around lines 12 - 33,
Add Zod runtime schemas for the request and response and use them to validate
wire data: create a PerplexitySearchRequestSchema (matching the
PerplexitySearchRequest interface) and a PerplexitySearchResponseSchema, export
their inferred types, and use PerplexitySearchRequestSchema.parse() to validate
the payload before sending in the client functions that accept
PerplexitySearchRequest; replace the current ad-hoc/runtime type assertions used
in the response parsing block (the response handling around the previous
"response parsing with runtime type assertions" area) with
PerplexitySearchResponseSchema.parse() to safely parse/throw on invalid
responses; ensure schemas cover optional fields (max_results,
max_tokens_per_page, search_domain_filter, search_recency_filter,
search_after_date_filter, search_before_date_filter) and include any nested
shapes asserted earlier, and update any tool definitions to reference the new
schemas.
| constructor(config: PerplexitySearchClientConfig = {}) { | ||
| this.apiKey = config.apiKey ?? getPerplexityApiKeyFromEnv() | ||
| this.baseURL = (config.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, '') |
There was a problem hiding this comment.
Reject blank explicit apiKey values in constructor config.
Line 60 accepts apiKey: '' (or whitespace), which bypasses env fallback and produces opaque downstream auth errors.
Suggested fix
constructor(config: PerplexitySearchClientConfig = {}) {
- this.apiKey = config.apiKey ?? getPerplexityApiKeyFromEnv()
+ this.apiKey =
+ typeof config.apiKey === 'string' && config.apiKey.trim().length > 0
+ ? config.apiKey.trim()
+ : getPerplexityApiKeyFromEnv()
this.baseURL = (config.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, '')
this.fetchImpl = config.fetch ?? globalThis.fetch
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| constructor(config: PerplexitySearchClientConfig = {}) { | |
| this.apiKey = config.apiKey ?? getPerplexityApiKeyFromEnv() | |
| this.baseURL = (config.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, '') | |
| constructor(config: PerplexitySearchClientConfig = {}) { | |
| this.apiKey = | |
| typeof config.apiKey === 'string' && config.apiKey.trim().length > 0 | |
| ? config.apiKey.trim() | |
| : getPerplexityApiKeyFromEnv() | |
| this.baseURL = (config.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, '') |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai-perplexity/src/search/client.ts` around lines 59 - 61,
The constructor currently accepts blank strings for config.apiKey which bypasses
getPerplexityApiKeyFromEnv and causes opaque auth errors; in the
PerplexitySearchClient constructor validate config.apiKey: if config.apiKey is
provided but after trimming is empty, throw a clear Error (or reject) stating
the apiKey is invalid; otherwise use the trimmed config.apiKey or fallback to
getPerplexityApiKeyFromEnv() to set this.apiKey. Reference the constructor,
PerplexitySearchClientConfig, apiKey, and getPerplexityApiKeyFromEnv when making
the change.
| if (!request.query || typeof request.query !== 'string') { | ||
| throw new Error('PerplexitySearchClient.search requires a non-empty `query`.') | ||
| } | ||
| validateDomainFilter(request.search_domain_filter) | ||
|
|
||
| const body: Record<string, unknown> = { query: request.query } | ||
| if (request.max_results !== undefined) body.max_results = request.max_results | ||
| if (request.max_tokens_per_page !== undefined) | ||
| body.max_tokens_per_page = request.max_tokens_per_page |
There was a problem hiding this comment.
Strengthen input validation for query and max_results.
Line 69 allows whitespace-only queries, and Line 75 forwards max_results without enforcing the documented 1–20 bounds from Line 15.
Suggested fix
- if (!request.query || typeof request.query !== 'string') {
+ if (typeof request.query !== 'string' || request.query.trim().length === 0) {
throw new Error('PerplexitySearchClient.search requires a non-empty `query`.')
}
+ if (
+ request.max_results !== undefined &&
+ (!Number.isInteger(request.max_results) ||
+ request.max_results < 1 ||
+ request.max_results > 20)
+ ) {
+ throw new Error('`max_results` must be an integer between 1 and 20.')
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!request.query || typeof request.query !== 'string') { | |
| throw new Error('PerplexitySearchClient.search requires a non-empty `query`.') | |
| } | |
| validateDomainFilter(request.search_domain_filter) | |
| const body: Record<string, unknown> = { query: request.query } | |
| if (request.max_results !== undefined) body.max_results = request.max_results | |
| if (request.max_tokens_per_page !== undefined) | |
| body.max_tokens_per_page = request.max_tokens_per_page | |
| if (typeof request.query !== 'string' || request.query.trim().length === 0) { | |
| throw new Error('PerplexitySearchClient.search requires a non-empty `query`.') | |
| } | |
| if ( | |
| request.max_results !== undefined && | |
| (!Number.isInteger(request.max_results) || | |
| request.max_results < 1 || | |
| request.max_results > 20) | |
| ) { | |
| throw new Error('`max_results` must be an integer between 1 and 20.') | |
| } | |
| validateDomainFilter(request.search_domain_filter) | |
| const body: Record<string, unknown> = { query: request.query } | |
| if (request.max_results !== undefined) body.max_results = request.max_results | |
| if (request.max_tokens_per_page !== undefined) | |
| body.max_tokens_per_page = request.max_tokens_per_page |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai-perplexity/src/search/client.ts` around lines 69 - 77,
PerplexitySearchClient.search currently accepts whitespace-only queries and
forwards max_results without enforcing the documented 1–20 bounds; update the
input validation in the search implementation to trim request.query and reject
it if the trimmed string is empty (throwing a clear Error), and validate
request.max_results (when defined) to be an integer between 1 and 20 inclusive
(throwing an Error if out of range) before assigning to the body; keep existing
validateDomainFilter usage and only set body.max_results after this new
validation.
| const { | ||
| name, | ||
| description, | ||
| defaultMaxResults, | ||
| ...clientConfig | ||
| } = config |
There was a problem hiding this comment.
Validate defaultMaxResults before using it as a fallback.
An out-of-range value (e.g. 0 or 50) bypasses model-provided schema limits and can generate invalid Search API requests.
Proposed guard
const {
name,
description,
defaultMaxResults,
...clientConfig
} = config
+
+ if (
+ defaultMaxResults !== undefined &&
+ (!Number.isInteger(defaultMaxResults) ||
+ defaultMaxResults < 1 ||
+ defaultMaxResults > 20)
+ ) {
+ throw new Error('defaultMaxResults must be an integer between 1 and 20.')
+ }Also applies to: 121-121
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai-perplexity/src/search/tool.ts` around lines 31 - 36,
The destructured defaultMaxResults from config must be validated before being
used as a fallback: ensure defaultMaxResults is a finite integer and within the
model/schema allowed range (e.g., >= min and <= max provided by the model or a
safe capped constant); if it is missing or out-of-range (0, negative, or too
large) ignore it and fall back to the model-provided limits or a safe cap. Add
this validation where defaultMaxResults is extracted (the const {
defaultMaxResults, ...clientConfig } = config) and again before the other usage
site referenced in the review (the place that builds the Search API request
around line ~121), so the request never sends an invalid maxResults value. Use
clear checks (Number.isInteger, bounds) and a single source of truth for the
allowed min/max to apply consistently.
| const key = env?.PERPLEXITY_API_KEY || env?.PPLX_API_KEY | ||
|
|
||
| if (!key) { | ||
| throw new Error( | ||
| 'PERPLEXITY_API_KEY (or PPLX_API_KEY) is required. Set it in your environment or pass an explicit apiKey.', | ||
| ) | ||
| } | ||
|
|
||
| return key |
There was a problem hiding this comment.
Reject whitespace-only API keys before returning.
On Line 15, a value like ' ' is treated as valid and returned, which later yields confusing auth failures. Trim and validate before returning.
Suggested fix
- const key = env?.PERPLEXITY_API_KEY || env?.PPLX_API_KEY
+ const key = [env?.PERPLEXITY_API_KEY, env?.PPLX_API_KEY].find(
+ (value): value is string =>
+ typeof value === 'string' && value.trim().length > 0,
+ )
...
- return key
+ return key.trim()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const key = env?.PERPLEXITY_API_KEY || env?.PPLX_API_KEY | |
| if (!key) { | |
| throw new Error( | |
| 'PERPLEXITY_API_KEY (or PPLX_API_KEY) is required. Set it in your environment or pass an explicit apiKey.', | |
| ) | |
| } | |
| return key | |
| const key = [env?.PERPLEXITY_API_KEY, env?.PPLX_API_KEY].find( | |
| (value): value is string => | |
| typeof value === 'string' && value.trim().length > 0, | |
| ) | |
| if (!key) { | |
| throw new Error( | |
| 'PERPLEXITY_API_KEY (or PPLX_API_KEY) is required. Set it in your environment or pass an explicit apiKey.', | |
| ) | |
| } | |
| return key.trim() |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai-perplexity/src/utils/api-key.ts` around lines 15 - 23,
The current API key selection returns whitespace-only values (const key =
env?.PERPLEXITY_API_KEY || env?.PPLX_API_KEY) which should be rejected; update
the logic in the api-key util so you trim the chosen value, treat
empty/whitespace-only strings as missing (e.g., if (!trimmedKey) throw the
existing Error), and return the trimmedKey instead of the raw value—apply this
change to the function that reads/returns the key in
packages/typescript/ai-perplexity/src/utils/api-key.ts.
| beforeEach(() => { | ||
| process.env.PERPLEXITY_API_KEY = 'test-key' | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| vi.restoreAllMocks() | ||
| }) |
There was a problem hiding this comment.
Restore environment variables in afterEach to avoid cross-test leakage.
The suite mutates process.env.PERPLEXITY_API_KEY but never restores prior state, which can cause order-dependent behavior in other tests.
Proposed fix
describe('perplexitySearchTool', () => {
+ const originalPerplexityApiKey = process.env.PERPLEXITY_API_KEY
+
beforeEach(() => {
process.env.PERPLEXITY_API_KEY = 'test-key'
})
afterEach(() => {
vi.restoreAllMocks()
+ if (originalPerplexityApiKey === undefined) {
+ delete process.env.PERPLEXITY_API_KEY
+ } else {
+ process.env.PERPLEXITY_API_KEY = originalPerplexityApiKey
+ }
})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai-perplexity/tests/search-tool.test.ts` around lines 5 -
11, The test mutates process.env.PERPLEXITY_API_KEY but doesn't restore it;
modify the beforeEach/afterEach in search-tool.test.ts to save the original
value into a local variable (e.g., let prevPerplexityKey) in beforeEach and then
restore it in afterEach (set process.env.PERPLEXITY_API_KEY back to
prevPerplexityKey or delete it if undefined) while keeping vi.restoreAllMocks()
intact.
Adds
@tanstack/ai-perplexity— a tree-shakeable adapter package for the Perplexity Search API plus an OpenAI-compatible chat client.What
perplexitySearchTool) —POST https://api.perplexity.ai/searchwrapped as a TanStack AItoolDefinition, ready to drop into achat({ tools: [...] })agent loop. Returns{ title, url, snippet, date? }per result. Exposesmax_results,search_domain_filter,search_recency_filter, and date filters in the input schema, and rejects requests that mix allow- + deny-list domain entries.PerplexitySearchClient— low-level Search API HTTP client for callers who don't want the tool wrapping.createPerplexityChatClient— thin factory that returns anopenaiSDK client withbaseURLset tohttps://api.perplexity.ai, so any code that already targets the OpenAI Chat Completions API can target Perplexity by swapping the base URL.PERPLEXITY_API_KEY, falling back toPPLX_API_KEY.Files
packages/typescript/ai-perplexity/— new package (search client + tool, OpenAI-compatible chat client, README).docs/adapters/perplexity.md— adapter docs page.docs/config.json— wired into the Adapters section.Tests
18 unit tests with mocked
fetchcover:/searchwith bearer auth and JSON bodymax_results,max_tokens_per_page, domain/recency/date filterssearch_domain_filterPERPLEXITY_API_KEY/PPLX_API_KEYenv-var fallbackdatefield omitted when API does not return onebaseURLhonoredmax_resultsapplied when caller omits itRun:
pnpm --filter @tanstack/ai-perplexity test:libDocs
Get a Perplexity API key at https://www.perplexity.ai/account/api/keys.
Summary by CodeRabbit
New Features
Documentation
Tests