Skip to content

feat: add Perplexity Search integration#516

Open
james-pplx wants to merge 1 commit intoTanStack:mainfrom
james-pplx:feat/perplexity
Open

feat: add Perplexity Search integration#516
james-pplx wants to merge 1 commit intoTanStack:mainfrom
james-pplx:feat/perplexity

Conversation

@james-pplx
Copy link
Copy Markdown

@james-pplx james-pplx commented Apr 29, 2026

Adds @tanstack/ai-perplexity — a tree-shakeable adapter package for the Perplexity Search API plus an OpenAI-compatible chat client.

What

  • Search API tool (perplexitySearchTool) — POST https://api.perplexity.ai/search wrapped as a TanStack AI toolDefinition, ready to drop into a chat({ tools: [...] }) agent loop. Returns { title, url, snippet, date? } per result. Exposes max_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 an openai SDK client with baseURL set to https://api.perplexity.ai, so any code that already targets the OpenAI Chat Completions API can target Perplexity by swapping the base URL.
  • API key resolved from PERPLEXITY_API_KEY, falling back to PPLX_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 fetch cover:

  • POST to /search with bearer auth and JSON body
  • Pass-through of max_results, max_tokens_per_page, domain/recency/date filters
  • Allow + deny mixing guard for search_domain_filter
  • Empty query rejection
  • PERPLEXITY_API_KEY / PPLX_API_KEY env-var fallback
  • Non-2xx error surface
  • date field omitted when API does not return one
  • Custom baseURL honored
  • Tool default name/description/schema
  • Tool default max_results applied when caller omits it
  • OpenAI-compatible chat client construction + env-var fallback

Run: pnpm --filter @tanstack/ai-perplexity test:lib

Docs

Get a Perplexity API key at https://www.perplexity.ai/account/api/keys.

Summary by CodeRabbit

  • New Features

    • Added Perplexity integration package enabling web search with live results for AI agents
    • Added OpenAI-compatible chat client for Perplexity API integration
    • Supports configurable API key management and custom endpoints
  • Documentation

    • Added comprehensive adapter documentation and usage examples
    • Updated navigation configuration for new Perplexity adapter
  • Tests

    • Added test coverage for chat client and search functionality

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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Walkthrough

This pull request introduces @tanstack/ai-perplexity, a new TanStack AI adapter package providing integration with Perplexity's Search API and OpenAI-compatible chat completions. It includes a search tool for agents, a chat client factory, utility functions for API key resolution, comprehensive documentation, and test coverage.

Changes

Cohort / File(s) Summary
Documentation
docs/adapters/perplexity.md, docs/config.json, packages/typescript/ai-perplexity/README.md
Added Perplexity adapter documentation with installation, API key setup, examples for search tools and chat clients, and navigation config entry.
Package Configuration
packages/typescript/ai-perplexity/package.json, packages/typescript/ai-perplexity/tsconfig.json, packages/typescript/ai-perplexity/vite.config.ts
Configured new package manifest with ESM exports and subpath entry points, TypeScript compiler settings, and Vitest/Vite build configuration.
Search Module
packages/typescript/ai-perplexity/src/search/client.ts, packages/typescript/ai-perplexity/src/search/tool.ts, packages/typescript/ai-perplexity/src/search/index.ts
Implemented low-level HTTP search client with request/response types and validation logic, TanStack AI tool wrapper with JSON schema definitions, and barrel exports.
Chat Module
packages/typescript/ai-perplexity/src/chat/client.ts, packages/typescript/ai-perplexity/src/chat/index.ts
Created OpenAI SDK wrapper factory configured with Perplexity API endpoint and environment-based API key resolution, with barrel export.
Utilities & Main Entry
packages/typescript/ai-perplexity/src/utils/api-key.ts, packages/typescript/ai-perplexity/src/utils/index.ts, packages/typescript/ai-perplexity/src/index.ts
Added API key environment variable resolution utility and consolidated public API exports across all modules.
Test Suite
packages/typescript/ai-perplexity/tests/chat-client.test.ts, packages/typescript/ai-perplexity/tests/search-client.test.ts, packages/typescript/ai-perplexity/tests/search-tool.test.ts
Implemented unit tests validating search client HTTP behavior, domain filter validation, API key precedence, chat client configuration, tool metadata, and schema compliance.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • AlemTuzlak

Poem

🐰 Whiskers twitching with glee,
A Perplexity adapter takes its place with care,
Search tools and chat clients hop through the air,
Web results grounded, API keys resolved with flair,
TanStack grows stronger, one adapter at a time!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description is comprehensive and covers motivation, implementation details, and testing, but the author has not completed the required template checklist items. Complete the template checklist by confirming you followed the Contributing guide and ran pnpm run test:pr, and by confirming whether a changeset was generated.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding a Perplexity Search integration package.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (2)
packages/typescript/ai-perplexity/tests/search-client.test.ts (1)

105-111: ⚡ Quick win

Consider adding boundary tests for stricter input validation paths.

If you adopt stricter validation in PerplexitySearchClient.search, add tests for whitespace-only query and out-of-range max_results to 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 win

Add 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

📥 Commits

Reviewing files that changed from the base of the PR and between ff33855 and 9366075.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (17)
  • docs/adapters/perplexity.md
  • docs/config.json
  • packages/typescript/ai-perplexity/README.md
  • packages/typescript/ai-perplexity/package.json
  • packages/typescript/ai-perplexity/src/chat/client.ts
  • packages/typescript/ai-perplexity/src/chat/index.ts
  • packages/typescript/ai-perplexity/src/index.ts
  • packages/typescript/ai-perplexity/src/search/client.ts
  • packages/typescript/ai-perplexity/src/search/index.ts
  • packages/typescript/ai-perplexity/src/search/tool.ts
  • packages/typescript/ai-perplexity/src/utils/api-key.ts
  • packages/typescript/ai-perplexity/src/utils/index.ts
  • packages/typescript/ai-perplexity/tests/chat-client.test.ts
  • packages/typescript/ai-perplexity/tests/search-client.test.ts
  • packages/typescript/ai-perplexity/tests/search-tool.test.ts
  • packages/typescript/ai-perplexity/tsconfig.json
  • packages/typescript/ai-perplexity/vite.config.ts

"vite": "^7.2.7"
},
"peerDependencies": {
"@tanstack/ai": "workspace:^"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -50

Repository: 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.

Suggested change
"@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:*".

Comment on lines +36 to +40
const { apiKey, baseURL, ...rest } = config
return new OpenAI({
...rest,
apiKey: apiKey ?? getPerplexityApiKeyFromEnv(),
baseURL: baseURL ?? DEFAULT_BASE_URL,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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`.

Comment on lines +12 to +33
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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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/src

Repository: 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 +1

Repository: 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 -20

Repository: 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 -10

Repository: 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_modules

Repository: 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.ts

Repository: 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.ts

Repository: 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 -40

Repository: 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.ts

Repository: 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 -100

Repository: 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 -30

Repository: 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.ts

Repository: TanStack/ai

Length of output: 111


🏁 Script executed:

# Check the length of search/client.ts
wc -l packages/typescript/ai-perplexity/src/search/client.ts

Repository: 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.ts

Repository: 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 client

Repository: 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 -80

Repository: 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.ts

Repository: 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: PerplexitySearchRequest interface
  • 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.

Comment on lines +59 to +61
constructor(config: PerplexitySearchClientConfig = {}) {
this.apiKey = config.apiKey ?? getPerplexityApiKeyFromEnv()
this.baseURL = (config.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, '')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +69 to +77
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested 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
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.

Comment on lines +31 to +36
const {
name,
description,
defaultMaxResults,
...clientConfig
} = config
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +15 to +23
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +5 to +11
beforeEach(() => {
process.env.PERPLEXITY_API_KEY = 'test-key'
})

afterEach(() => {
vi.restoreAllMocks()
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant