import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 7860;
app.use(express.json());
// Mandatory Endpoint: /health
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', service: 'Velo' });
});
// Mandatory Endpoint: /api-docs
app.get('/api-docs', (req, res) => {
const html = `
Velo API Documentation
Velo API Documentation
Velo - AI-powered novel writing IDE API Endpoints
GET /health
Purpose: Readiness and liveness check for Hugging Face Spaces.
Response Example:
{
"status": "ok",
"service": "Velo"
}
GET /api-docs
Purpose: Returns documentation for all available API endpoints.
POST /predict
Purpose: Predict story continuation or next scene elements based on context input.
Request Example:
{
"prompt": "The old clock struck midnight as Elena entered the library...",
"maxLength": 100
}
Response Example:
{
"continuation": "A faint blue light gleamed from behind the bookshelf...",
"status": "success"
}
POST /generate
Purpose: Generate narrative suggestions or plot outlines.
Request Example:
{
"genre": "Sci-Fi",
"topic": "First contact with an ancient artifact"
}
Response Example:
{
"outline": "Chapter 1: Discovery in the Kuiper Belt...",
"status": "success"
}
GET /v1/models
Purpose: OpenAI-compatible models listing endpoint.
Response Example:
{
"object": "list",
"data": [
{ "id": "velo-default", "object": "model", "created": 1700000000, "owned_by": "velo" }
]
}
POST /v1/chat/completions
Purpose: OpenAI-compatible chat completions endpoint. Supports proxying to a custom upstream OpenAI-compatible Base URL via custom_url / base_url parameter or header.
Request Example:
{
"model": "velo-default",
"messages": [
{ "role": "user", "content": "Write a short scene about a wizard." }
],
"custom_url": "https://api.openai.com/v1"
}
Response Example:
{
"id": "chatcmpl-velo-1234567890",
"object": "chat.completion",
"created": 1700000000,
"model": "velo-default",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The old wizard carefully opened the ancient spellbook..."
},
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 10, "completion_tokens": 15, "total_tokens": 25 }
}
`;
res.setHeader('Content-Type', 'text/html');
res.send(html);
});
// Functional Endpoint: /predict
app.post('/predict', (req, res) => {
const { prompt, maxLength = 100 } = req.body || {};
if (!prompt) {
return res.status(400).json({ error: 'Missing required parameter: prompt' });
}
res.json({
continuation: `[Velo Model Output for: "${prompt.slice(0, 30)}..."] A mysterious shadow glided across the floor, leaving behind glowing cyan runes.`,
maxLength,
status: 'success'
});
});
// Functional Endpoint: /generate
app.post('/generate', (req, res) => {
const { genre = 'General', topic = 'Unspecified' } = req.body || {};
res.json({
genre,
topic,
outline: `[Generated Outline for ${genre} - ${topic}]:\n1. Introduction of characters\n2. Inciting incident\n3. Climax & Resolution`,
status: 'success'
});
});
// OpenAI-Compatible Endpoint: /v1/models
app.get('/v1/models', (req, res) => {
res.json({
object: 'list',
data: [
{ id: 'velo-default', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'velo' },
{ id: 'gpt-4o', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'velo' },
{ id: 'deepseek-chat', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'velo' }
]
});
});
// OpenAI-Compatible Endpoint: /v1/chat/completions with Custom URL Support
app.post('/v1/chat/completions', async (req, res) => {
const { model = 'velo-default', messages = [], custom_url, base_url } = req.body || {};
// Check if a custom upstream URL is specified in body, header, or query
const targetUrl = custom_url || base_url || req.headers['x-custom-url'] || req.headers['x-base-url'];
if (targetUrl) {
try {
// Normalize upstream URL to ensure /chat/completions path
let cleanBase = String(targetUrl).replace(/\/+$/, '');
if (!cleanBase.endsWith('/chat/completions')) {
if (!cleanBase.endsWith('/v1')) cleanBase += '/v1';
cleanBase += '/chat/completions';
}
const upstreamHeaders = {
'Content-Type': 'application/json',
};
if (req.headers.authorization) {
upstreamHeaders['Authorization'] = req.headers.authorization;
}
const upstreamBody = { ...req.body };
delete upstreamBody.custom_url;
delete upstreamBody.base_url;
const response = await fetch(cleanBase, {
method: 'POST',
headers: upstreamHeaders,
body: JSON.stringify(upstreamBody),
});
const responseData = await response.json();
return res.status(response.status).json(responseData);
} catch (err) {
return res.status(502).json({
error: {
message: `Failed to proxy to custom URL: ${err.message}`,
type: 'proxy_error',
code: 502
}
});
}
}
// Fallback response when no custom upstream URL is proxied
const lastUserMsg = messages.slice().reverse().find((m) => m.role === 'user')?.content || 'Hello';
const responseContent = `[Velo AI Response] Story continuation & creative suggestion for input: "${String(lastUserMsg).slice(0, 50)}"`;
res.json({
id: `chatcmpl-velo-${Date.now()}`,
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: responseContent
},
finish_reason: 'stop'
}
],
usage: {
prompt_tokens: lastUserMsg.length,
completion_tokens: responseContent.length,
total_tokens: lastUserMsg.length + responseContent.length
}
});
});
// Serve static frontend build from dist folder
const distPath = path.join(__dirname, 'dist');
app.use(express.static(distPath));
// Fallback to index.html for SPA routing (Express 5 compatible)
app.use((req, res) => {
res.sendFile(path.join(distPath, 'index.html'));
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`Velo app listening on port ${PORT}`);
});