RSS Git Download  Clone
Raw Blame History 2kB 59 lines
import OpenAI from 'openai';
import { zodToJsonSchema } from './schema.mjs';

// USD per 1M tokens. Update when prices change.
const RATES = {
    'gpt-5.5': { in: 5.00, out: 30.00 },
    'gpt-5': { in: 3.00, out: 10.00 },
    'gpt-5-mini': { in: 0.30, out: 1.20 },
    'gpt-5-nano': { in: 0.05, out: 0.20 },
    'gpt-4o': { in: 2.50, out: 10.00 },
};

let client = null;
const getClient = () => {
    if (!client) {
        if (!process.env.OPENAI_API_KEY) {
            throw new Error('OPENAI_API_KEY missing — see secure/.env.example');
        }
        client = new OpenAI();
    }
    return client;
};

export async function callOpenAI({ system, user, schema, schemaName, model = process.env.OPENAI_MODEL || 'gpt-5.5' }) {
    const response = await getClient().chat.completions.create({
        model,
        messages: [
            { role: 'system', content: system },
            { role: 'user', content: user },
        ],
        response_format: {
            type: 'json_schema',
            json_schema: {
                name: schemaName,
                strict: true,
                schema: zodToJsonSchema(schema),
            },
        },
    });

    const content = response.choices[0]?.message?.content;
    if (!content) {
        throw new Error('OpenAI returned empty content');
    }

    const parsed = JSON.parse(content);
    const validated = schema.parse(parsed);

    const rates = RATES[model] || RATES['gpt-5.5'];
    const usage = {
        input: response.usage.prompt_tokens,
        output: response.usage.completion_tokens,
        usd: (response.usage.prompt_tokens * rates.in + response.usage.completion_tokens * rates.out) / 1_000_000,
        model,
    };

    return { data: validated, usage };
}