import { NextRequest, NextResponse } from 'next/server'; import { getStalwartCredentials } from '@/lib/stalwart/credentials'; import { configManager } from '@/lib/admin/config-manager'; import { findOpencodeServer } from '@/lib/ai/opencode'; export const runtime = 'nodejs'; /** * GET /api/ai/opencode/models — models a locally-running `opencode serve` * exposes. Proxied rather than fetched directly by the renderer: the desktop * shell's origin is a random localhost port that changes every launch, so a * direct call would need opencode's CORS allowlist updated each time. * * Listing is not a billable action, so a valid session is enough — no seat * check (matching /api/ai/server/models). */ export async function GET(request: NextRequest) { const auth = await getStalwartCredentials(request); if (!auth) { return NextResponse.json({ error: 'not authenticated' }, { status: 401 }); } await configManager.ensureLoaded(); if (configManager.getAiConsoleConfig().classesEnabled.opencode === false) { return NextResponse.json({ error: 'the OpenCode class is disabled by admin policy' }, { status: 403 }); } const found = await findOpencodeServer(); if (!found) { // 503 not 500: "nothing is listening" is a normal state (opencode simply // isn't running), and the client turns it into setup guidance rather than // an error banner. return NextResponse.json( { error: 'No local OpenCode server is running. The desktop app starts one automatically when the opencode CLI is installed \u2014 install it from opencode.ai, then restart VNCmail+.' }, { status: 503 }, ); } return NextResponse.json( { models: found.models.map((m) => ({ ref: m.ref, label: m.label })) }, { headers: { 'Cache-Control': 'no-store' } }, ); }