jan/website/scripts/generate-cloud-spec.js

422 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
// Get current directory in ES modules
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const CONFIG = {
// Jan Server API spec URL - change this for different environments
JAN_SERVER_SPEC_URL:
process.env.JAN_SERVER_SPEC_URL ||
'https://api.jan.ai/api/swagger/doc.json',
// Server URLs for different environments
SERVERS: {
production: {
url: process.env.JAN_SERVER_PROD_URL || 'https://api.jan.ai/v1',
description: 'Jan Server API (Production)',
},
staging: {
url:
process.env.JAN_SERVER_STAGING_URL || 'https://staging-api.jan.ai/v1',
description: 'Jan Server API (Staging)',
},
local: {
url: process.env.JAN_SERVER_LOCAL_URL || 'http://localhost:8000/v1',
description: 'Jan Server (Local Development)',
},
minikube: {
url:
process.env.JAN_SERVER_MINIKUBE_URL ||
'http://jan-server.local:8000/v1',
description: 'Jan Server (Minikube)',
},
},
// Output file path
OUTPUT_PATH: path.join(__dirname, '../public/openapi/cloud-openapi.json'),
// Fallback to local spec if fetch fails
FALLBACK_SPEC_PATH: path.join(__dirname, '../public/openapi/openapi.json'),
// Request timeout in milliseconds
FETCH_TIMEOUT: 10000,
}
// Model examples for Jan Server (vLLM deployment)
const MODEL_EXAMPLES = [
'llama-3.1-8b-instruct',
'mistral-7b-instruct-v0.3',
'gemma-2-9b-it',
'qwen2.5-7b-instruct',
]
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
cyan: '\x1b[36m',
bright: '\x1b[1m',
}
function log(message, type = 'info') {
const prefix =
{
success: `${colors.green}`,
warning: `${colors.yellow}⚠️ `,
error: `${colors.red}`,
info: `${colors.cyan} `,
}[type] || ''
console.log(`${prefix} ${message}${colors.reset}`)
}
async function fetchWithTimeout(url, options = {}) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), CONFIG.FETCH_TIMEOUT)
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
})
clearTimeout(timeoutId)
return response
} catch (error) {
clearTimeout(timeoutId)
throw error
}
}
// =============================================================================
// SPEC ENHANCEMENT FUNCTIONS
// =============================================================================
function enhanceSpecWithBranding(spec) {
// Update info section with Jan Server branding
spec.info = {
...spec.info,
'title': '👋Jan Server API',
'description':
'OpenAI-compatible API for Jan Server powered by vLLM. High-performance, scalable inference service with automatic batching and optimized memory management.',
'version': spec.info?.version || '1.0.0',
'x-logo': {
url: 'https://jan.ai/logo.png',
altText: '👋Jan Server API',
},
'contact': {
name: 'Jan Server Support',
url: 'https://jan.ai/support',
email: 'support@jan.ai',
},
'license': {
name: 'Apache 2.0',
url: 'https://github.com/menloresearch/jan/blob/main/LICENSE',
},
}
// Update servers with our configured endpoints
spec.servers = Object.values(CONFIG.SERVERS)
// Add global security requirement
spec.security = [{ bearerAuth: [] }]
// Add tags for better organization
spec.tags = [
{ name: 'Models', description: 'List and describe available models' },
{
name: 'Chat',
description: 'Chat completion endpoints for conversational AI',
},
{ name: 'Completions', description: 'Text completion endpoints' },
{ name: 'Embeddings', description: 'Generate embeddings for text' },
{ name: 'Usage', description: 'Monitor API usage and quotas' },
]
return spec
}
function enhanceSecuritySchemes(spec) {
if (!spec.components) spec.components = {}
if (!spec.components.securitySchemes) spec.components.securitySchemes = {}
spec.components.securitySchemes.bearerAuth = {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description:
'Enter your Jan Server API key. Configure authentication in your server settings.',
}
return spec
}
function addModelExamples(spec) {
const primaryModel = MODEL_EXAMPLES[0]
// Helper function to update model fields in schemas
function updateModelField(modelField) {
if (!modelField) return
modelField.example = primaryModel
modelField.description = `ID of the model to use. Available models: ${MODEL_EXAMPLES.join(', ')}`
if (modelField.anyOf && modelField.anyOf[0]?.type === 'string') {
modelField.anyOf[0].example = primaryModel
modelField.anyOf[0].enum = MODEL_EXAMPLES
} else if (modelField.type === 'string') {
modelField.enum = MODEL_EXAMPLES
}
}
// Update model fields in common request schemas
const schemas = spec.components?.schemas || {}
if (schemas.CreateCompletionRequest?.properties?.model) {
updateModelField(schemas.CreateCompletionRequest.properties.model)
}
if (schemas.CreateChatCompletionRequest?.properties?.model) {
updateModelField(schemas.CreateChatCompletionRequest.properties.model)
}
if (schemas.CreateEmbeddingRequest?.properties?.model) {
updateModelField(schemas.CreateEmbeddingRequest.properties.model)
}
return spec
}
function addRequestExamples(spec) {
const primaryModel = MODEL_EXAMPLES[0]
// Example request bodies
const examples = {
completion: {
'text-completion': {
summary: 'Text Completion Example',
description: `Complete text using ${primaryModel}`,
value: {
model: primaryModel,
prompt: 'Once upon a time,',
max_tokens: 50,
temperature: 0.7,
top_p: 0.9,
stream: false,
},
},
},
chatCompletion: {
'simple-chat': {
summary: 'Simple Chat Example',
description: `Chat completion using ${primaryModel}`,
value: {
model: primaryModel,
messages: [
{ role: 'user', content: 'What is the capital of France?' },
],
max_tokens: 100,
temperature: 0.7,
stream: false,
},
},
},
embedding: {
'text-embedding': {
summary: 'Text Embedding Example',
description: `Generate embeddings using ${primaryModel}`,
value: {
model: primaryModel,
input: 'The quick brown fox jumps over the lazy dog',
},
},
},
}
// Add examples to path operations
Object.keys(spec.paths || {}).forEach((path) => {
Object.keys(spec.paths[path] || {}).forEach((method) => {
const operation = spec.paths[path][method]
if (!operation.requestBody?.content?.['application/json']) return
if (path.includes('/completions') && !path.includes('/chat')) {
operation.requestBody.content['application/json'].examples =
examples.completion
} else if (path.includes('/chat/completions')) {
operation.requestBody.content['application/json'].examples =
examples.chatCompletion
} else if (path.includes('/embeddings')) {
operation.requestBody.content['application/json'].examples =
examples.embedding
}
})
})
return spec
}
function addCloudFeatures(spec) {
// Add cloud-specific extension
spec['x-jan-server-features'] = {
vllm: {
version: '0.5.0',
features: [
'PagedAttention for efficient memory management',
'Continuous batching for high throughput',
'Tensor parallelism for multi-GPU serving',
'Quantization support (AWQ, GPTQ, SqueezeLLM)',
'Speculative decoding',
'LoRA adapter support',
],
},
scaling: {
auto_scaling: true,
min_replicas: 1,
max_replicas: 100,
target_qps: 100,
},
limits: {
max_tokens_per_request: 32768,
max_batch_size: 256,
timeout_seconds: 300,
},
}
return spec
}
// =============================================================================
// MAIN FUNCTIONS
// =============================================================================
async function fetchJanServerSpec() {
log(`Fetching Jan Server spec from: ${CONFIG.JAN_SERVER_SPEC_URL}`)
try {
const response = await fetchWithTimeout(CONFIG.JAN_SERVER_SPEC_URL)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const spec = await response.json()
log('Successfully fetched Jan Server specification', 'success')
return spec
} catch (error) {
log(`Failed to fetch Jan Server spec: ${error.message}`, 'warning')
// If FORCE_UPDATE is set, don't use fallback - fail instead
if (process.env.FORCE_UPDATE === 'true') {
log('Force update requested - not using fallback', 'error')
throw error
}
log(`Falling back to local spec: ${CONFIG.FALLBACK_SPEC_PATH}`, 'warning')
if (fs.existsSync(CONFIG.FALLBACK_SPEC_PATH)) {
const fallbackSpec = JSON.parse(
fs.readFileSync(CONFIG.FALLBACK_SPEC_PATH, 'utf8')
)
log('Using local fallback specification', 'warning')
return fallbackSpec
} else {
throw new Error('No fallback spec available')
}
}
}
async function generateCloudSpec() {
console.log(
`${colors.bright}${colors.cyan}🚀 Jan Server API Spec Generator${colors.reset}`
)
console.log(
`${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}`
)
console.log(`📡 Source: ${CONFIG.JAN_SERVER_SPEC_URL}`)
console.log(`📁 Output: ${CONFIG.OUTPUT_PATH}`)
console.log(`🏗️ Servers: ${Object.keys(CONFIG.SERVERS).join(', ')}`)
console.log('')
try {
// Fetch the real Jan Server specification
let spec = await fetchJanServerSpec()
// Apply all enhancements
spec = enhanceSpecWithBranding(spec)
spec = enhanceSecuritySchemes(spec)
spec = addModelExamples(spec)
spec = addRequestExamples(spec)
spec = addCloudFeatures(spec)
// Ensure all paths have security requirements
Object.keys(spec.paths || {}).forEach((path) => {
Object.keys(spec.paths[path] || {}).forEach((method) => {
const operation = spec.paths[path][method]
if (!operation.security) {
operation.security = [{ bearerAuth: [] }]
}
})
})
// Write the enhanced specification
fs.writeFileSync(CONFIG.OUTPUT_PATH, JSON.stringify(spec, null, 2), 'utf8')
log('Jan Server specification generated successfully!', 'success')
console.log(`📁 Output: ${CONFIG.OUTPUT_PATH}`)
console.log('\n📊 Summary:')
console.log(` - Endpoints: ${Object.keys(spec.paths || {}).length}`)
console.log(` - Servers: ${spec.servers?.length || 0}`)
console.log(` - Models: ${MODEL_EXAMPLES.length}`)
console.log(` - Security: Bearer token authentication`)
console.log(
` - Engine: vLLM (${spec['x-jan-server-features']?.vllm?.version || 'unknown'})`
)
return true
} catch (error) {
log(
`Failed to generate Jan Server specification: ${error.message}`,
'error'
)
console.log('\n🔧 Troubleshooting:')
console.log(' 1. Check your internet connection')
console.log(
` 2. Verify Jan Server is accessible at: ${CONFIG.JAN_SERVER_SPEC_URL}`
)
console.log(' 3. Check if you need to set environment variables:')
console.log(' - JAN_SERVER_SPEC_URL')
console.log(' - JAN_SERVER_PROD_URL')
console.log(' - JAN_SERVER_LOCAL_URL')
return false
}
}
// =============================================================================
// EXECUTION
// =============================================================================
// Show configuration on startup
if (process.env.NODE_ENV !== 'test') {
console.log(`${colors.cyan}🔧 Configuration:${colors.reset}`)
console.log(` Spec URL: ${CONFIG.JAN_SERVER_SPEC_URL}`)
console.log(` Timeout: ${CONFIG.FETCH_TIMEOUT}ms`)
console.log(` Servers: ${Object.keys(CONFIG.SERVERS).length} configured`)
if (process.env.FORCE_UPDATE === 'true') {
console.log(` ${colors.yellow}Force Update: ENABLED${colors.reset}`)
}
console.log('')
}
// Run the generator
const success = await generateCloudSpec()
process.exit(success ? 0 : 1)