Skip to content

Commit db5bd3f

Browse files
jliounisPSI Bot
authored andcommitted
fix: address Perplexity adapter review feedback
1 parent 3c9517b commit db5bd3f

9 files changed

Lines changed: 119 additions & 9 deletions

File tree

packages/typescript/ai-perplexity/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"openai": "^6.9.1"
5252
},
5353
"devDependencies": {
54-
"@tanstack/ai": "workspace:*",
54+
"@tanstack/ai": "workspace:^",
5555
"@vitest/coverage-v8": "4.0.14",
5656
"vite": "^7.2.7"
5757
},

packages/typescript/ai-perplexity/src/chat/client.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,14 @@ export function createPerplexityChatClient(
3434
config: PerplexityChatClientConfig = {},
3535
): OpenAI {
3636
const { apiKey, baseURL, ...rest } = config
37+
const resolvedApiKey =
38+
typeof apiKey === 'string' && apiKey.trim().length > 0
39+
? apiKey
40+
: getPerplexityApiKeyFromEnv()
41+
3742
return new OpenAI({
3843
...rest,
39-
apiKey: apiKey ?? getPerplexityApiKeyFromEnv(),
44+
apiKey: resolvedApiKey,
4045
baseURL: baseURL ?? DEFAULT_BASE_URL,
4146
})
4247
}

packages/typescript/ai-perplexity/src/search/client.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,13 @@ export class PerplexitySearchClient {
5757
private readonly fetchImpl: typeof fetch
5858

5959
constructor(config: PerplexitySearchClientConfig = {}) {
60-
this.apiKey = config.apiKey ?? getPerplexityApiKeyFromEnv()
60+
const { apiKey } = config
61+
const resolvedApiKey =
62+
typeof apiKey === 'string' && apiKey.trim().length > 0
63+
? apiKey
64+
: getPerplexityApiKeyFromEnv()
65+
66+
this.apiKey = resolvedApiKey
6167
this.baseURL = (config.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, '')
6268
this.fetchImpl = config.fetch ?? globalThis.fetch
6369
}
@@ -66,13 +72,15 @@ export class PerplexitySearchClient {
6672
request: PerplexitySearchRequest,
6773
init: { signal?: AbortSignal } = {},
6874
): Promise<PerplexitySearchResponse> {
69-
if (!request.query || typeof request.query !== 'string') {
75+
const query = typeof request.query === 'string' ? request.query.trim() : ''
76+
if (query.length === 0) {
7077
throw new Error('PerplexitySearchClient.search requires a non-empty `query`.')
7178
}
7279
validateDomainFilter(request.search_domain_filter)
7380

74-
const body: Record<string, unknown> = { query: request.query }
75-
if (request.max_results !== undefined) body.max_results = request.max_results
81+
const body: Record<string, unknown> = { query }
82+
if (request.max_results !== undefined)
83+
body.max_results = clampMaxResults(request.max_results)
7684
if (request.max_tokens_per_page !== undefined)
7785
body.max_tokens_per_page = request.max_tokens_per_page
7886
if (request.search_domain_filter)
@@ -119,6 +127,13 @@ export class PerplexitySearchClient {
119127
}
120128
}
121129

130+
function clampMaxResults(maxResults: number): number {
131+
if (!Number.isFinite(maxResults)) {
132+
throw new Error('max_results must be a finite number.')
133+
}
134+
return Math.min(20, Math.max(1, Math.trunc(maxResults)))
135+
}
136+
122137
function validateDomainFilter(filter: Array<string> | undefined): void {
123138
if (!filter || filter.length === 0) return
124139
let hasAllow = false

packages/typescript/ai-perplexity/src/search/tool.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ export function perplexitySearchTool(
3434
defaultMaxResults,
3535
...clientConfig
3636
} = config
37+
if (
38+
defaultMaxResults !== undefined &&
39+
(!Number.isInteger(defaultMaxResults) ||
40+
defaultMaxResults < 1 ||
41+
defaultMaxResults > 20)
42+
) {
43+
throw new Error('defaultMaxResults must be an integer between 1 and 20.')
44+
}
3745

3846
// Lazily construct the client so missing API keys don't blow up at import
3947
// time (e.g. on bundlers that statically evaluate module top-level).

packages/typescript/ai-perplexity/src/utils/api-key.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ export function getPerplexityApiKeyFromEnv(): string {
1212
? process.env
1313
: undefined
1414

15-
const key = env?.PERPLEXITY_API_KEY || env?.PPLX_API_KEY
15+
const key = [env?.PERPLEXITY_API_KEY, env?.PPLX_API_KEY]
16+
.find(
17+
(value): value is string =>
18+
typeof value === 'string' && value.trim().length > 0,
19+
)
20+
?.trim()
1621

1722
if (!key) {
1823
throw new Error(

packages/typescript/ai-perplexity/tests/chat-client.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ describe('createPerplexityChatClient', () => {
2424
expect(client.apiKey).toBe('explicit')
2525
})
2626

27+
it('falls back to env when explicit apiKey is blank', () => {
28+
const client = createPerplexityChatClient({ apiKey: ' ' })
29+
expect(client.apiKey).toBe('test-key')
30+
})
31+
2732
it('falls back to PPLX_API_KEY when PERPLEXITY_API_KEY is not set', () => {
2833
delete process.env.PERPLEXITY_API_KEY
2934
process.env.PPLX_API_KEY = 'fallback'

packages/typescript/ai-perplexity/tests/search-client.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ describe('PerplexitySearchClient', () => {
6363
})
6464
})
6565

66+
it('falls back to env when explicit apiKey is blank', async () => {
67+
const fetchMock = makeFetchMock({ results: [] })
68+
const client = new PerplexitySearchClient({
69+
apiKey: ' ',
70+
fetch: fetchMock as any,
71+
})
72+
73+
await client.search({ query: 'q' })
74+
75+
const headers = fetchMock.mock.calls[0]![1].headers as Record<string, string>
76+
expect(headers.Authorization).toBe('Bearer test-key')
77+
})
78+
6679
it('forwards optional filters in the request body', async () => {
6780
const fetchMock = makeFetchMock({ results: [] })
6881
const client = new PerplexitySearchClient({ fetch: fetchMock as any })
@@ -110,6 +123,36 @@ describe('PerplexitySearchClient', () => {
110123
).rejects.toThrow(/non-empty `query`/i)
111124
})
112125

126+
it('throws when query is whitespace only', async () => {
127+
const fetchMock = makeFetchMock({ results: [] })
128+
const client = new PerplexitySearchClient({ fetch: fetchMock as any })
129+
await expect(client.search({ query: ' ' })).rejects.toThrow(
130+
/non-empty `query`/i,
131+
)
132+
expect(fetchMock).not.toHaveBeenCalled()
133+
})
134+
135+
it('trims query and clamps max_results before forwarding', async () => {
136+
const fetchMock = makeFetchMock({ results: [] })
137+
const client = new PerplexitySearchClient({ fetch: fetchMock as any })
138+
await client.search({ query: ' mars rover ', max_results: 99 })
139+
140+
const body = JSON.parse(fetchMock.mock.calls[0]![1].body as string)
141+
expect(body).toEqual({
142+
query: 'mars rover',
143+
max_results: 20,
144+
})
145+
})
146+
147+
it('clamps max_results to the minimum before forwarding', async () => {
148+
const fetchMock = makeFetchMock({ results: [] })
149+
const client = new PerplexitySearchClient({ fetch: fetchMock as any })
150+
await client.search({ query: 'q', max_results: 0 })
151+
152+
const body = JSON.parse(fetchMock.mock.calls[0]![1].body as string)
153+
expect(body.max_results).toBe(1)
154+
})
155+
113156
it('falls back to PPLX_API_KEY when PERPLEXITY_API_KEY is not set', async () => {
114157
delete process.env.PERPLEXITY_API_KEY
115158
process.env.PPLX_API_KEY = 'fallback-key'
@@ -120,6 +163,16 @@ describe('PerplexitySearchClient', () => {
120163
expect(headers.Authorization).toBe('Bearer fallback-key')
121164
})
122165

166+
it('ignores whitespace-only PERPLEXITY_API_KEY when PPLX_API_KEY is set', async () => {
167+
process.env.PERPLEXITY_API_KEY = ' '
168+
process.env.PPLX_API_KEY = 'fallback-key'
169+
const fetchMock = makeFetchMock({ results: [] })
170+
const client = new PerplexitySearchClient({ fetch: fetchMock as any })
171+
await client.search({ query: 'q' })
172+
const headers = fetchMock.mock.calls[0]![1].headers as Record<string, string>
173+
expect(headers.Authorization).toBe('Bearer fallback-key')
174+
})
175+
123176
it('throws if neither env var is set and no apiKey is passed', () => {
124177
delete process.env.PERPLEXITY_API_KEY
125178
delete process.env.PPLX_API_KEY

packages/typescript/ai-perplexity/tests/search-tool.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
22
import { perplexitySearchTool } from '../src/search/tool'
33

44
describe('perplexitySearchTool', () => {
5+
const ORIGINAL_PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY
6+
57
beforeEach(() => {
68
process.env.PERPLEXITY_API_KEY = 'test-key'
79
})
810

911
afterEach(() => {
12+
if (ORIGINAL_PERPLEXITY_API_KEY === undefined) {
13+
delete process.env.PERPLEXITY_API_KEY
14+
} else {
15+
process.env.PERPLEXITY_API_KEY = ORIGINAL_PERPLEXITY_API_KEY
16+
}
1017
vi.restoreAllMocks()
1118
})
1219

@@ -101,6 +108,18 @@ describe('perplexitySearchTool', () => {
101108
})
102109
})
103110

111+
it('throws when defaultMaxResults is outside the allowed range', () => {
112+
expect(() => perplexitySearchTool({ defaultMaxResults: 0 })).toThrow(
113+
/integer between 1 and 20/i,
114+
)
115+
expect(() => perplexitySearchTool({ defaultMaxResults: 21 })).toThrow(
116+
/integer between 1 and 20/i,
117+
)
118+
expect(() => perplexitySearchTool({ defaultMaxResults: 1.5 })).toThrow(
119+
/integer between 1 and 20/i,
120+
)
121+
})
122+
104123
it('honors custom name and description overrides', () => {
105124
const tool = perplexitySearchTool({
106125
apiKey: 'k',

pnpm-lock.yaml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)