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
JavaScript
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 };
}