422 lines
12 KiB
JavaScript
422 lines
12 KiB
JavaScript
#!/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)
|