import 'server-only'; import type { CTPDocument } from "@/lib/ctp/schema"; import type { AnalysisInput, AnalysisProvider } from "@/lib/analysis/providers"; import { CTP_SCHEMA, SYSTEM_PROMPT } from "./anthropic"; function buildLabel(): string { const model = process.env.OPENAI_MODEL ?? "GPT-4o"; const baseUrl = process.env.OPENAI_BASE_URL ?? ""; if (baseUrl && !baseUrl.includes("api.openai.com")) { try { const host = new URL(baseUrl).hostname; return `${model} (${host})`; } catch { // fall through } } return `${model} (OpenAI)`; } export const openaiProvider: AnalysisProvider = { id: "openai", label: buildLabel(), type: "cloud-ai", async isAvailable() { if (process.env.OPENAI_API_KEY) { return { available: true }; } return { available: false, reason: "OPENAI_API_KEY not set" }; }, async generateCTP(input: AnalysisInput): Promise { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error("OPENAI_API_KEY not set"); } const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"; const model = process.env.OPENAI_MODEL ?? "gpt-4o"; const { bpm, duration, title, artist, mbid, contributed_by } = input; const approxBars = Math.round((duration * bpm) / 60 / 4); const userMessage = `\ Generate a CTP document for the following song: Title: ${title ?? "Unknown Title"} Artist: ${artist ?? "Unknown Artist"} MusicBrainz ID: ${mbid ?? "unknown"} Detected BPM: ${bpm} Duration: ${duration.toFixed(1)} seconds (~${approxBars} bars at 4/4) Contributed by: ${contributed_by} Create a plausible section layout for this song. If this is a well-known song, use your knowledge of its actual arrangement. If not, use a sensible generic structure.`; const response = await fetch(`${baseUrl}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ model, messages: [ { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: userMessage }, ], response_format: { type: "json_schema", json_schema: { strict: true, name: "CTPDocument", schema: CTP_SCHEMA, }, }, max_tokens: 2048, }), }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error(`OpenAI API error ${response.status}: ${text.slice(0, 200)}`); } const json = await response.json() as { choices?: Array<{ message?: { content?: string } }>; }; const content = json.choices?.[0]?.message?.content; if (!content) { throw new Error("OpenAI did not return a message content"); } let parsed: unknown; try { parsed = JSON.parse(content); } catch { throw new Error(`OpenAI returned invalid JSON: ${content.slice(0, 200)}`); } const doc = parsed as CTPDocument; if (!doc.metadata.created_at || doc.metadata.created_at.includes("placeholder")) { doc.metadata.created_at = new Date().toISOString(); } return doc; }, };