UNPKG

gepa-spo

Version:

Genetic-Pareto prompt optimizer to evolve system prompts from a few rollouts with modular support and intelligent crossover

83 lines (82 loc) 3.79 kB
import { silentLogger } from './logger.js'; /** Minimal OpenAI client (global fetch). Responses API for actor; Chat Completions for judge. */ export function makeOpenAIClients(cfg, logger = silentLogger) { const base = cfg.baseURL ?? 'https://api.openai.com/v1'; const key = cfg.apiKey ?? process.env.OPENAI_API_KEY; if (!key) throw new Error('OPENAI_API_KEY missing'); async function http(path, body) { const controller = new AbortController(); const timeoutMs = cfg.requestTimeoutMs ?? 60_000; const timer = setTimeout(() => controller.abort(new Error(`HTTP timeout after ${timeoutMs}ms for ${path}`)), timeoutMs); try { const r = await fetch(base + path, { method: 'POST', headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: controller.signal }); const j = await r.json(); if (!r.ok) throw new Error(`OpenAI ${r.status}: ${JSON.stringify(j)}`); logger.debug(`HTTP ${path} ok`); return j; } finally { clearTimeout(timer); } } // Responses API: recommended for GPT-5 series async function responses(model, input, opts) { const isGpt5 = /^gpt-5/i.test(model); const body = { model, input, temperature: opts?.temperature ?? cfg.temperature ?? 0.7, max_output_tokens: opts?.maxTokens ?? cfg.maxTokens ?? 512 }; if (isGpt5) { delete body.temperature; body.reasoning = { effort: opts?.reasoningEffort ?? 'minimal' }; } logger.debug(`Responses call model=${model}`); const j = await http('/responses', body); if (typeof j.output_text === 'string' && j.output_text.trim().length > 0) return j.output_text; if (Array.isArray(j.output)) { for (const item of j.output) { if (item?.type === 'message' && Array.isArray(item.content)) { const firstText = item.content.find(p => typeof p.text === 'string' && p.text.trim().length > 0)?.text; if (firstText) return firstText; } } } if (j.choices?.[0]?.message?.content) return j.choices[0].message.content; return JSON.stringify(j); } // Chat Completions: judge (multi-message) async function chat(model, messages, opts) { const isGpt5 = /^gpt-5/i.test(model); // For GPT-5 models, use the Responses API instead of Chat Completions (reasoning params not supported there) if (isGpt5) { const prompt = messages.map(m => `${m.role.toUpperCase()}:\n${m.content}`).join('\n\n'); return responses(model, prompt, { maxTokens: opts?.maxTokens ?? cfg.maxTokens ?? 512, temperature: opts?.temperature ?? cfg.temperature ?? 0.2, reasoningEffort: 'minimal' }); } const body = { model, messages, temperature: opts?.temperature ?? cfg.temperature ?? 0.2, // Chat Completions expects `max_tokens` max_tokens: opts?.maxTokens ?? cfg.maxTokens ?? 512 }; // noisy body print removed; rely on debug logger above logger.debug(`Chat call model=${model}`); const j = await http('/chat/completions', body); return j.choices?.[0]?.message?.content ?? ''; } const actorLLM = { complete: (prompt) => responses(cfg.actorModel, prompt) }; const chatLLM = { chat: (messages, opts) => chat(cfg.judgeModel, messages, opts) }; return { actorLLM, chatLLM }; }