← back to AbramsOS

lib/chat-providers/openai.js

63 lines

// OpenAI adapter — EXTERNAL lane. Mirrors the proven `ask-openai` call path
// (Responses API, gpt-5.2). Prompt is already redacted + NER-scrubbed by the
// route before it reaches here (AGENTS.md: no PII to a model provider without
// redaction). AGENTS.md bans only the ANTHROPIC key — OpenAI is permitted.

const BASE = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
const CHAT_TIMEOUT_MS = 60_000;

function key() {
  const k = process.env.OPENAI_API_KEY;
  if (!k) throw new Error('OPENAI_API_KEY not set');
  return k;
}

// Pull the text out of a Responses-API payload (output_text convenience field,
// else the output[].content[] blocks of type output_text).
function extractText(j) {
  if (typeof j.output_text === 'string' && j.output_text.trim()) return j.output_text.trim();
  const parts = [];
  for (const item of j.output || []) {
    for (const c of item.content || []) {
      if (c.type === 'output_text' && typeof c.text === 'string') parts.push(c.text);
    }
  }
  return parts.join('').trim();
}

async function chat({ system, messages, providerModel }) {
  const model = providerModel || 'gpt-5.2';
  // Responses API accepts an array of {role, content} as `input`; `instructions`
  // carries the system prompt.
  const input = (messages || []).map((m) => ({
    role: m.role === 'assistant' ? 'assistant' : 'user',
    content: String(m.content ?? ''),
  }));

  const ctrl = new AbortController();
  const t = setTimeout(() => ctrl.abort(), CHAT_TIMEOUT_MS);
  try {
    const r = await fetch(`${BASE}/responses`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${key()}`,
      },
      body: JSON.stringify({ model, instructions: system || undefined, input }),
      signal: ctrl.signal,
    });
    if (!r.ok) {
      const body = (await r.text().catch(() => '')).slice(0, 300);
      throw new Error(`openai ${r.status}: ${body}`);
    }
    const j = await r.json();
    const text = extractText(j);
    if (!text) throw new Error('openai returned empty response');
    return { text };
  } finally {
    clearTimeout(t);
  }
}

module.exports = { chat, extractText, BASE };