From 77757ace5e45fa30c291c8fc101efb253b08f352 Mon Sep 17 00:00:00 2001 From: Jose Henrique Date: Mon, 30 Mar 2026 19:58:14 -0300 Subject: [PATCH] adding support for generic AI provider (OpenAI compatible) --- packages/backend/.env | 10 ++++++++++ packages/backend/src/agent.ts | 19 ++++++++++++++++--- packages/backend/src/agents/curator.ts | 9 +++++---- packages/backend/src/agents/interpreter.ts | 6 +++--- packages/backend/src/agents/ranking.ts | 6 +++--- packages/backend/src/agents/retrieval.ts | 9 +++++---- packages/backend/src/agents/titleGenerator.ts | 6 +++--- 7 files changed, 45 insertions(+), 20 deletions(-) diff --git a/packages/backend/.env b/packages/backend/.env index a93c2a0..478c0a5 100644 --- a/packages/backend/.env +++ b/packages/backend/.env @@ -3,4 +3,14 @@ # In production / Docker, supply these as environment variables. DATABASE_URL=postgres://user:password@localhost:5432/recommender + +# AI provider selection: OPENAI (default) or GENERIC +AI_PROVIDER=OPENAI + +# OpenAI provider settings (used when AI_PROVIDER=OPENAI) OPENAI_API_KEY=your-openai-api-key-here + +# Generic provider settings (used when AI_PROVIDER=GENERIC) +PROVIDER_URL=https://your-provider.example.com/v1 +BEARER_TOKEN=your-bearer-token +MODEL_NAME=your-model-name diff --git a/packages/backend/src/agent.ts b/packages/backend/src/agent.ts index 97388a4..25bf842 100644 --- a/packages/backend/src/agent.ts +++ b/packages/backend/src/agent.ts @@ -2,9 +2,22 @@ import OpenAI from 'openai'; import * as dotenv from 'dotenv'; dotenv.config({ path: ['.env.local', '.env'] }); -export const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, -}); +const AI_PROVIDER = process.env.AI_PROVIDER ?? 'OPENAI'; +const isGeneric = AI_PROVIDER === 'GENERIC'; + +export const openai = isGeneric + ? new OpenAI({ + apiKey: process.env.BEARER_TOKEN, + baseURL: process.env.PROVIDER_URL, + }) + : new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, + }); + +export const defaultModel = isGeneric ? (process.env.MODEL_NAME ?? 'default') : 'gpt-5.4'; +export const miniModel = isGeneric ? (process.env.MODEL_NAME ?? 'default') : 'gpt-5.4-mini'; +export const serviceOptions = isGeneric ? {} : { service_tier: 'flex' as const }; +export const supportsWebSearch = !isGeneric; export async function askAgent(prompt: string) { try { diff --git a/packages/backend/src/agents/curator.ts b/packages/backend/src/agents/curator.ts index a7a202b..235e50b 100644 --- a/packages/backend/src/agents/curator.ts +++ b/packages/backend/src/agents/curator.ts @@ -1,4 +1,4 @@ -import { openai } from '../agent.js'; +import { openai, defaultModel, serviceOptions, supportsWebSearch } from '../agent.js'; import type { InterpreterOutput, RankingOutput, CuratorOutput, MediaType } from '../types/agents.js'; import { z } from 'zod'; import { zodTextFormat } from 'openai/helpers/zod'; @@ -32,11 +32,12 @@ export async function runCurator( .map((s) => `- "${s.title}" (${s.category})`) .join('\n'); + const canSearch = useWebSearch && supportsWebSearch; const response = await openai.responses.parse({ - model: 'gpt-5.4', + model: defaultModel, temperature: 0.5, - service_tier: 'flex', - ...(useWebSearch ? { tools: [{ type: 'web_search' as const }] } : {}), + ...serviceOptions, + ...(canSearch ? { tools: [{ type: 'web_search' as const }] } : {}), text: { format: zodTextFormat(CuratorSchema, "shows") }, instructions: `You are a ${mediaLabel} recommendation curator. For each ${mediaLabel}, write a concise 1-2 sentence explanation of why it was assigned to its category based on the user's preferences.${useWebSearch ? '\n\nUse web search to verify details and enrich explanations with accurate information.' : ''} diff --git a/packages/backend/src/agents/interpreter.ts b/packages/backend/src/agents/interpreter.ts index a6c7bd7..6638198 100644 --- a/packages/backend/src/agents/interpreter.ts +++ b/packages/backend/src/agents/interpreter.ts @@ -1,4 +1,4 @@ -import { openai } from '../agent.js'; +import { openai, defaultModel, serviceOptions } from '../agent.js'; import type { InterpreterOutput, MediaType } from '../types/agents.js'; import { z } from 'zod'; import { zodTextFormat } from 'openai/helpers/zod'; @@ -28,9 +28,9 @@ export async function runInterpreter(input: InterpreterInput): Promise `- ${c.title}: ${c.reason}`).join('\n'); const response = await openai.responses.parse({ - model: 'gpt-5.4', + model: defaultModel, temperature: 0.2, - service_tier: 'flex', + ...serviceOptions, text: { format: zodTextFormat(RankingSchema, "ranking") }, instructions: `You are a ${mediaLabel} ranking critic. Assign each ${mediaLabel} to exactly one of four confidence buckets based on how well it matches the user's preferences. diff --git a/packages/backend/src/agents/retrieval.ts b/packages/backend/src/agents/retrieval.ts index 8e96ca8..b0ec073 100644 --- a/packages/backend/src/agents/retrieval.ts +++ b/packages/backend/src/agents/retrieval.ts @@ -1,4 +1,4 @@ -import { openai } from '../agent.js'; +import { openai, defaultModel, serviceOptions, supportsWebSearch } from '../agent.js'; import type { InterpreterOutput, RetrievalOutput, MediaType } from '../types/agents.js'; import { z } from 'zod'; import { zodTextFormat } from 'openai/helpers/zod'; @@ -19,11 +19,12 @@ export async function runRetrieval( const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show'; const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV shows'; + const canSearch = useWebSearch && supportsWebSearch; const response = await openai.responses.parse({ - model: 'gpt-5.4', + model: defaultModel, temperature: 0.9, - service_tier: 'flex', - ...(useWebSearch ? { tools: [{ type: 'web_search' as const }] } : {}), + ...serviceOptions, + ...(canSearch ? { tools: [{ type: 'web_search' as const }] } : {}), text: { format: zodTextFormat(RetrievalSchema, "candidates") }, instructions: `You are a ${mediaLabel} candidate generator. Your goal is to brainstorm a LARGE, DIVERSE pool of ${brainstormCount} ${mediaLabel} candidates that match the user's structured preferences.${useWebSearch ? '\n\nUse web search to find recent and accurate titles, including newer releases.' : ''} diff --git a/packages/backend/src/agents/titleGenerator.ts b/packages/backend/src/agents/titleGenerator.ts index 1b8375f..3ddad61 100644 --- a/packages/backend/src/agents/titleGenerator.ts +++ b/packages/backend/src/agents/titleGenerator.ts @@ -1,13 +1,13 @@ -import { openai } from '../agent.js'; +import { openai, miniModel, serviceOptions } from '../agent.js'; import type { InterpreterOutput, MediaType } from '../types/agents.js'; export async function generateTitle(interpreter: InterpreterOutput, mediaType: MediaType = 'tv_show'): Promise { const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show'; const response = await openai.responses.create({ - model: 'gpt-5.4-mini', + model: miniModel, temperature: 0.7, - service_tier: 'flex', + ...serviceOptions, instructions: `Generate a concise 5-8 word title for a ${mediaLabel} recommendation session. Capture the essence of the user's taste — genre, tone, key themes. Respond with ONLY the title. No quotes, no trailing punctuation.