Skip to content

Commit 7dd4d61

Browse files
committed
feat: migrate AI from Perplexity to OpenAI (gpt-5-search-api)
Remove Perplexity as an AI provider and consolidate all AI tasks on OpenAI. Web-grounded tasks (artistSummary, albumDetail, genreSummary, artistSentence, albumRecommendations) now use gpt-5-search-api via the Chat Completions API, which reliably returns url_citation annotations for source attribution. Key changes: - Delete packages/services/ai/src/perplexity.ts (PerplexityClient removed) - Switch 5 web search tasks from Perplexity sonar to gpt-5-search-api - Route gpt-5-search-api through Chat Completions with web_search_options - Add web_search_options support to chatCompletionViaChatCompletions() - Add citation link-to-number replacement in parseResponsesResult() - Remove PERPLEXITY_API_KEY from Bindings, Env, wrangler.toml (web + bot) - Remove perplexity from AIProvider type, RATE_LIMITS, DB schema - Remove SearchContextSize type and searchContextSize from prompt files - Simplify AIService to OpenAI-only (remove perplexityApiKey, getClientForTask) - Increase OpenAI rate limit from 60 to 90 req/min - Add xhigh to ReasoningEffort type for GPT-5.2 Why gpt-5-search-api over GPT-5.2 Responses API web_search: GPT-5.2 with reasoning returns empty annotations[] (agentic search). Even with reasoning:'none', Responses API annotations are unreliable. gpt-5-search-api via Chat Completions always returns url_citation annotations. Docs updated: CLAUDE.md, README.md, ai-models.md, migration doc, about page. Tests updated: removed Perplexity tests, updated mocks for Chat Completions.
1 parent b243281 commit 7dd4d61

File tree

24 files changed

+219
-462
lines changed

24 files changed

+219
-462
lines changed

CLAUDE.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,7 @@ Required secrets in `apps/web/wrangler.toml`:
9393
| `SPOTIFY_REFRESH_TOKEN` | Spotify API |
9494
| `LASTFM_API_KEY` | Last.fm API (read-only) |
9595
| `LASTFM_SHARED_SECRET` | Last.fm API (for authentication) |
96-
| `OPENAI_API_KEY` | GPT models |
97-
| `PERPLEXITY_API_KEY` | Web-grounded AI |
96+
| `OPENAI_API_KEY` | GPT models (including web search) |
9897
| `INTERNAL_API_SECRET` | Internal API tokens |
9998
| `ADMIN_SECRET` | Admin endpoints |
10099
| `DISCORD_WEBHOOK_URL` | Discord notifications (user signups) |
@@ -110,8 +109,7 @@ Discord bot secrets in `apps/discord-bot/wrangler.toml`:
110109
| `SPOTIFY_CLIENT_SECRET` | Spotify API |
111110
| `SPOTIFY_REFRESH_TOKEN` | Spotify API |
112111
| `LASTFM_API_KEY` | Last.fm API |
113-
| `OPENAI_API_KEY` | AI summaries |
114-
| `PERPLEXITY_API_KEY` | AI summaries |
112+
| `OPENAI_API_KEY` | AI summaries (including web search) |
115113
| `APPLE_KEY_ID` | Apple MusicKit (for StreamingLinksService) |
116114
| `APPLE_TEAM_ID` | Apple MusicKit |
117115
| `APPLE_PRIVATE_KEY` | Apple MusicKit (PEM format) |
@@ -426,8 +424,7 @@ app.route('/', myInternalRoutes); // Results in /api/internal/my-internal-data
426424

427425
| Provider | Models | Best For |
428426
|----------|--------|----------|
429-
| Perplexity | `sonar` | Web-grounded responses with citations |
430-
| OpenAI | `gpt-5.1`, `gpt-5-mini`, `gpt-5-nano` | Reasoning, creative tasks, coding |
427+
| OpenAI | `gpt-5-search-api`, `gpt-5.2`, `gpt-5.1`, `gpt-5-mini` | All AI tasks including web search with citations |
431428

432429
Key points:
433430
- Pass `internalToken` to Layout for pages using internal APIs
@@ -561,8 +558,7 @@ npx wrangler tail --format=json
561558

562559
**AI responses failing:**
563560
- Check API keys in `.dev.vars` (local) or wrangler secrets (prod)
564-
- Perplexity rate limit: 30 req/min
565-
- OpenAI rate limit: 60 req/min
561+
- OpenAI rate limit: 90 req/min
566562

567563
**Session/authentication issues:**
568564
- Session cookie not set - check `LASTFM_SHARED_SECRET` is configured

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ A music discovery platform that combines real-time listening data with AI-powere
1616
- **Album & Artist Search** - Search the Spotify catalog with instant results
1717
- **Album Detail Pages** - Rich album pages with release info, track listings, genres, and AI-generated summaries with citations
1818
- **Artist Detail Pages** - Artist profiles with biography, top albums, similar artists, and genre connections
19-
- **AI-Powered Summaries** - Get rich, contextual information about any artist, album, or genre powered by Perplexity AI (with source citations)
19+
- **AI-Powered Summaries** - Get rich, contextual information about any artist, album, or genre powered by OpenAI GPT-5.2 with web search (with source citations)
2020
- **Genre Exploration** - Discover music by genre with AI-generated descriptions, history, and key artists
2121
- **Cross-Platform Streaming Links** - Every album includes direct links to Spotify, Apple Music, and Songlink (for all other services)
2222
- **Album Recommendations** - AI-generated "if you like this, try these" recommendations on album pages
@@ -79,7 +79,7 @@ listentomore/
7979
│ ├── services/ # Backend service modules
8080
│ │ ├── spotify/ # Spotify Web API client
8181
│ │ ├── lastfm/ # Last.fm API client
82-
│ │ ├── ai/ # OpenAI + Perplexity clients
82+
│ │ ├── ai/ # OpenAI client
8383
│ │ ├── songlink/ # Odesli/Songlink API client
8484
│ │ └── streaming-links/ # Cross-platform link service (Apple Music, URL parsing)
8585
│ ├── db/ # D1 schema, migrations, queries
@@ -122,7 +122,6 @@ SPOTIFY_REFRESH_TOKEN=your_spotify_refresh_token
122122
LASTFM_API_KEY=your_lastfm_api_key
123123
LASTFM_USERNAME=your_lastfm_username
124124
OPENAI_API_KEY=your_openai_api_key
125-
PERPLEXITY_API_KEY=your_perplexity_api_key
126125
INTERNAL_API_SECRET=your_random_secret_for_internal_apis
127126

128127
# Optional - for admin features
@@ -157,8 +156,7 @@ pnpm deploy
157156

158157
- **[Spotify Web API](https://developer.spotify.com/documentation/web-api)** - Music catalog data
159158
- **[Last.fm API](https://www.last.fm/api)** - Listening history and scrobbles
160-
- **[OpenAI API](https://platform.openai.com)** - GPT-5 for chatbot and fact generation
161-
- **[Perplexity API](https://docs.perplexity.ai)** - Sonar model for grounded, cited summaries
159+
- **[OpenAI API](https://platform.openai.com)** - GPT-5.2 for AI summaries, web search with citations, chatbot, and fact generation
162160
- **[Apple MusicKit API](https://developer.apple.com/musickit/)** - Cross-platform streaming links via UPC matching
163161
- **[Songlink/Odesli](https://odesli.co)** - Cross-platform streaming links for all services
164162

apps/discord-bot/src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ interface Env {
3333
SPOTIFY_REFRESH_TOKEN: string;
3434
LASTFM_API_KEY: string;
3535
OPENAI_API_KEY: string;
36-
PERPLEXITY_API_KEY: string;
3736
// Apple MusicKit credentials (for streaming links)
3837
APPLE_KEY_ID: string;
3938
APPLE_TEAM_ID: string;
@@ -75,7 +74,6 @@ function createServices(env: Env): Services {
7574
}),
7675
ai: new AIService({
7776
openaiApiKey: env.OPENAI_API_KEY,
78-
perplexityApiKey: env.PERPLEXITY_API_KEY,
7977
cache: env.CACHE,
8078
}),
8179
};

apps/discord-bot/wrangler.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ vars = { ENVIRONMENT = "production" }
3737
# - SPOTIFY_REFRESH_TOKEN
3838
# - LASTFM_API_KEY
3939
# - OPENAI_API_KEY
40-
# - PERPLEXITY_API_KEY
4140
# Apple MusicKit (for streaming links):
4241
# - APPLE_KEY_ID
4342
# - APPLE_TEAM_ID

apps/web/src/__tests__/services/ai.test.ts

Lines changed: 16 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// AIService integration tests
22

33
import { describe, it, expect, vi, beforeEach } from 'vitest';
4-
import { PerplexityClient, OpenAIClient, AICache, generateArtistSummary } from '@listentomore/ai';
4+
import { OpenAIClient, AICache, generateArtistSummary } from '@listentomore/ai';
55
import { createMockKV, setupFetchMock } from '../utils/mocks';
66

77
describe('OpenAIClient', () => {
@@ -159,79 +159,6 @@ describe('OpenAIClient', () => {
159159
});
160160
});
161161

162-
describe('PerplexityClient', () => {
163-
let client: PerplexityClient;
164-
165-
beforeEach(() => {
166-
client = new PerplexityClient('test-api-key');
167-
});
168-
169-
describe('chatCompletion', () => {
170-
it('sends chat completion request and returns response', async () => {
171-
const response = {
172-
choices: [{ message: { content: 'This is a response about Radiohead.' } }],
173-
citations: ['https://en.wikipedia.org/wiki/Radiohead'],
174-
};
175-
setupFetchMock([{ pattern: /api\.perplexity\.ai/, response }]);
176-
177-
const result = await client.chatCompletion({
178-
model: 'sonar',
179-
messages: [{ role: 'user', content: 'Tell me about Radiohead' }],
180-
});
181-
182-
expect(result.content).toBe('This is a response about Radiohead.');
183-
expect(result.citations).toEqual(['https://en.wikipedia.org/wiki/Radiohead']);
184-
});
185-
186-
it('preserves citation markers in response', async () => {
187-
const response = {
188-
choices: [{ message: { content: 'Radiohead [1] formed in 1985 [2] in Oxford.' } }],
189-
citations: ['https://example.com', 'https://example2.com'],
190-
};
191-
setupFetchMock([{ pattern: /api\.perplexity\.ai/, response }]);
192-
193-
const result = await client.chatCompletion({
194-
model: 'sonar',
195-
messages: [{ role: 'user', content: 'test' }],
196-
});
197-
198-
// Citation markers are preserved for client-side transformation to superscript links
199-
expect(result.content).toBe('Radiohead [1] formed in 1985 [2] in Oxford.');
200-
expect(result.citations).toEqual(['https://example.com', 'https://example2.com']);
201-
});
202-
203-
it('throws error on API failure', async () => {
204-
setupFetchMock([
205-
{
206-
pattern: /api\.perplexity\.ai/,
207-
response: { error: 'Unauthorized' },
208-
options: { status: 401, ok: false },
209-
},
210-
]);
211-
212-
await expect(
213-
client.chatCompletion({
214-
model: 'sonar',
215-
messages: [{ role: 'user', content: 'test' }],
216-
})
217-
).rejects.toThrow('Perplexity API error 401');
218-
});
219-
220-
it('handles empty citations', async () => {
221-
const response = {
222-
choices: [{ message: { content: 'Response without citations' } }],
223-
};
224-
setupFetchMock([{ pattern: /api\.perplexity\.ai/, response }]);
225-
226-
const result = await client.chatCompletion({
227-
model: 'sonar',
228-
messages: [{ role: 'user', content: 'test' }],
229-
});
230-
231-
expect(result.citations).toEqual([]);
232-
});
233-
});
234-
});
235162

236163
describe('AICache', () => {
237164
let mockKV: KVNamespace;
@@ -303,28 +230,37 @@ describe('AICache', () => {
303230

304231
describe('generateArtistSummary', () => {
305232
let mockKV: KVNamespace;
306-
let mockClient: PerplexityClient;
233+
let mockClient: OpenAIClient;
307234
let cache: AICache;
308235

309236
beforeEach(() => {
310237
mockKV = createMockKV();
311238
cache = new AICache(mockKV);
312-
mockClient = new PerplexityClient('test-key');
239+
mockClient = new OpenAIClient('test-key');
313240
});
314241

315242
it('generates artist summary and caches result', async () => {
316243
const response = {
244+
model: 'gpt-5-search-api',
317245
choices: [
318246
{
319247
message: {
320-
content:
321-
'Radiohead is an English rock band. Their album {{OK Computer}} is considered a masterpiece. Similar artists include [[Portishead]].',
248+
content: 'Radiohead is an English rock band. Their album {{OK Computer}} is considered a masterpiece. Similar artists include [[Portishead]].',
249+
annotations: [
250+
{
251+
type: 'url_citation',
252+
url_citation: {
253+
url: 'https://en.wikipedia.org/wiki/Radiohead',
254+
title: 'Radiohead - Wikipedia',
255+
},
256+
},
257+
],
322258
},
323259
},
324260
],
325-
citations: ['https://en.wikipedia.org/wiki/Radiohead'],
261+
usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 },
326262
};
327-
setupFetchMock([{ pattern: /api\.perplexity\.ai/, response }]);
263+
setupFetchMock([{ pattern: /api\.openai\.com/, response }]);
328264

329265
const result = await generateArtistSummary('Radiohead', mockClient, cache);
330266

apps/web/src/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ app.use('*', async (c, next) => {
121121
'ai',
122122
new AIService({
123123
openaiApiKey: c.env.OPENAI_API_KEY,
124-
perplexityApiKey: c.env.PERPLEXITY_API_KEY,
125124
cache: c.env.CACHE,
126125
})
127126
);
@@ -749,7 +748,6 @@ async function scheduled(
749748
if (minute < 5) {
750749
const ai = new AIService({
751750
openaiApiKey: env.OPENAI_API_KEY,
752-
perplexityApiKey: env.PERPLEXITY_API_KEY,
753751
cache: env.CACHE,
754752
});
755753

apps/web/src/pages/about/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function AboutPage({ currentUser }: AboutPageProps) {
3030

3131
<h2>Nerdy details</h2>
3232
<p>
33-
The site uses APIs from Last.fm, Spotify, OpenAI, and Perplexity to get album and artist
33+
The site uses APIs from Last.fm, Spotify, and OpenAI to get album and artist
3434
data and generate some interesting facts about it all. It is built on{' '}
3535
<a href="https://cloudflare.com/" target="_blank" rel="noopener noreferrer">
3636
Cloudflare

apps/web/src/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export type Bindings = {
2525
LASTFM_SHARED_SECRET?: string;
2626
LASTFM_USERNAME: string;
2727
OPENAI_API_KEY: string;
28-
PERPLEXITY_API_KEY: string;
2928
YOUTUBE_API_KEY?: string;
3029
APPLE_TEAM_ID?: string;
3130
APPLE_KEY_ID?: string;

apps/web/wrangler.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ crons = ["*/5 * * * *"] # Run every 5 minutes (random fact only at top of hour)
5151
# - SPOTIFY_STREAMING_REFRESH_TOKEN
5252
# - LASTFM_API_KEY
5353
# - OPENAI_API_KEY
54-
# - PERPLEXITY_API_KEY
5554
# - DISCOGS_API_TOKEN
5655
# - YOUTUBE_API_KEY (for streaming links service)
5756
# - INTERNAL_API_SECRET (for signing internal API tokens)

docs/PERPLEXITY_TO_OPENAI_MIGRATION.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
# Perplexity to OpenAI Migration Plan
22

3-
Replace all Perplexity API calls with OpenAI GPT-5.2 (web search + citations) to consolidate on a single AI provider.
3+
Replace all Perplexity API calls with OpenAI to consolidate on a single AI provider.
4+
5+
> **Status: COMPLETED.** Web search tasks use `gpt-5-search-api` via Chat Completions API (not GPT-5.2 Responses API) for reliable citation annotations.
46
57
## Background
68

7-
Currently, ListenToMore uses **Perplexity** (`sonar` model) for 5 web-grounded tasks that need citations, and **OpenAI** for everything else. Both providers already normalize to the same `{ content: string, citations: string[] }` interface via our `ChatClient` abstraction, making this migration straightforward.
9+
ListenToMore previously used **Perplexity** (`sonar` model) for 5 web-grounded tasks that need citations, and **OpenAI** for everything else. Both providers normalized to the same `{ content: string, citations: string[] }` interface via our `ChatClient` abstraction, making this migration straightforward.
810

9-
### Target Model: GPT-5.2
11+
### Target Model: gpt-5-search-api
1012

11-
We're migrating to **GPT-5.2** (`gpt-5.2`), OpenAI's flagship model with:
12-
- **Web search support** via the Responses API
13-
- **Reasoning effort levels:** `none` (default), `low`, `medium`, `high`, `xhigh`
14-
- **400K context window**, 128K max output tokens
15-
- **Pricing:** $1.75/1M input, $14/1M output + $10/1K web search calls
16-
- **Knowledge cutoff:** August 31, 2025
13+
We migrated to **`gpt-5-search-api`** via the Chat Completions API:
14+
- **Always performs web search** and returns `url_citation` annotations reliably
15+
- **Uses Chat Completions API** with `web_search_options: {}` (not Responses API)
16+
- **Pricing:** Same as GPT-5 family + search call costs
1717

18-
GPT-5.2 with `reasoning: 'none'` provides fast, low-latency responses. For factual web searches where accuracy matters, `reasoning: 'low'` is recommended.
18+
**Why not GPT-5.2 with Responses API `web_search` tool:**
19+
- GPT-5.2 with any reasoning level uses "agentic search" which returns **empty** `annotations: []`
20+
- Even with `reasoning: 'none'`, Responses API annotations were unreliable
21+
- This is a known issue in the OpenAI developer community
22+
- `gpt-5-search-api` via Chat Completions is the reliable path for citations
1923

2024
### Tasks Currently on Perplexity
2125

0 commit comments

Comments
 (0)