jan/website/scripts/fix-local-spec-complete.js

747 lines
21 KiB
JavaScript

#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const cloudSpecPath = path.join(
__dirname,
'../public/openapi/cloud-openapi.json'
)
const outputPath = path.join(__dirname, '../public/openapi/openapi.json')
console.log(
'🔧 Fixing Local OpenAPI Spec with Complete Examples and Schemas...'
)
// Read cloud spec as a reference
const cloudSpec = JSON.parse(fs.readFileSync(cloudSpecPath, 'utf8'))
// Convert Swagger 2.0 to OpenAPI 3.0 format for paths
function convertSwaggerPathToOpenAPI3(swaggerPath) {
const openApiPath = {}
Object.keys(swaggerPath || {}).forEach((method) => {
if (typeof swaggerPath[method] === 'object') {
openApiPath[method] = {
...swaggerPath[method],
// Convert parameters
parameters: swaggerPath[method].parameters?.filter(
(p) => p.in !== 'body'
),
// Convert body parameter to requestBody
requestBody: swaggerPath[method].parameters?.find(
(p) => p.in === 'body'
)
? {
required: true,
content: {
'application/json': {
schema: swaggerPath[method].parameters.find(
(p) => p.in === 'body'
).schema,
},
},
}
: undefined,
// Convert responses
responses: {},
}
// Convert responses
Object.keys(swaggerPath[method].responses || {}).forEach((statusCode) => {
const response = swaggerPath[method].responses[statusCode]
openApiPath[method].responses[statusCode] = {
description: response.description,
content: response.schema
? {
'application/json': {
schema: response.schema,
},
}
: undefined,
}
})
}
})
return openApiPath
}
// Create comprehensive local spec
const localSpec = {
openapi: '3.1.0',
info: {
title: 'Jan API',
description:
"OpenAI-compatible API for local inference with Jan. Run AI models locally with complete privacy using llama.cpp's high-performance inference engine. Supports GGUF models with CPU and GPU acceleration. No authentication required for local usage.",
version: '0.3.14',
contact: {
name: 'Jan Support',
url: 'https://jan.ai/support',
email: 'support@jan.ai',
},
license: {
name: 'Apache 2.0',
url: 'https://github.com/janhq/jan/blob/main/LICENSE',
},
},
servers: [
{
url: 'http://127.0.0.1:1337',
description: 'Local Jan Server (Default IP)',
},
{
url: 'http://localhost:1337',
description: 'Local Jan Server (localhost)',
},
{
url: 'http://localhost:8080',
description: 'Local Jan Server (Alternative Port)',
},
],
tags: [
{
name: 'Models',
description: 'List and describe available models',
},
{
name: 'Chat',
description: 'Chat completion endpoints for conversational AI',
},
{
name: 'Completions',
description: 'Text completion endpoints for generating text',
},
{
name: 'Extras',
description:
'Additional utility endpoints for tokenization and text processing',
},
],
paths: {},
components: {
schemas: {},
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description:
'Optional: Enter your API key if authentication is enabled. The Bearer prefix will be added automatically.',
},
},
},
}
// Local model examples
const LOCAL_MODELS = [
'gemma-2-2b-it-Q8_0',
'llama-3.1-8b-instruct-Q4_K_M',
'mistral-7b-instruct-v0.3-Q4_K_M',
'phi-3-mini-4k-instruct-Q4_K_M',
]
// Add completions endpoint with rich examples
localSpec.paths['/v1/completions'] = {
post: {
tags: ['Completions'],
summary: 'Create completion',
description:
"Creates a completion for the provided prompt and parameters. This endpoint is compatible with OpenAI's completions API.",
operationId: 'create_completion',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/CreateCompletionRequest',
},
examples: {
basic: {
summary: 'Basic Completion',
description: 'Simple text completion example',
value: {
model: LOCAL_MODELS[0],
prompt: 'Once upon a time',
max_tokens: 50,
temperature: 0.7,
},
},
creative: {
summary: 'Creative Writing',
description: 'Generate creative content with higher temperature',
value: {
model: LOCAL_MODELS[0],
prompt: 'Write a short poem about coding:',
max_tokens: 150,
temperature: 1.0,
top_p: 0.95,
},
},
code: {
summary: 'Code Generation',
description: 'Generate code with lower temperature for accuracy',
value: {
model: LOCAL_MODELS[0],
prompt:
'# Python function to calculate fibonacci\ndef fibonacci(n):',
max_tokens: 200,
temperature: 0.3,
stop: ['\n\n', 'def ', 'class '],
},
},
streaming: {
summary: 'Streaming Response',
description: 'Stream tokens as they are generated',
value: {
model: LOCAL_MODELS[0],
prompt: 'Explain quantum computing in simple terms:',
max_tokens: 300,
temperature: 0.7,
stream: true,
},
},
},
},
},
},
responses: {
200: {
description: 'Successful Response',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/CreateCompletionResponse',
},
},
},
},
202: {
description: 'Accepted - Request is being processed',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/CreateCompletionResponse',
},
},
'text/event-stream': {
schema: {
type: 'string',
format: 'binary',
description: 'Server-sent events stream for streaming responses',
},
},
},
},
422: {
description: 'Validation Error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ValidationError',
},
},
},
},
},
},
}
// Add chat completions endpoint with rich examples
localSpec.paths['/v1/chat/completions'] = {
post: {
tags: ['Chat'],
summary: 'Create chat completion',
description:
"Creates a model response for the given chat conversation. This endpoint is compatible with OpenAI's chat completions API.",
operationId: 'create_chat_completion',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/CreateChatCompletionRequest',
},
examples: {
simple: {
summary: 'Simple Chat',
description: 'Basic question and answer',
value: {
model: LOCAL_MODELS[0],
messages: [
{
role: 'user',
content: 'What is the capital of France?',
},
],
max_tokens: 100,
temperature: 0.7,
},
},
system: {
summary: 'With System Message',
description: 'Chat with system instructions',
value: {
model: LOCAL_MODELS[0],
messages: [
{
role: 'system',
content:
'You are a helpful assistant that speaks like a pirate.',
},
{
role: 'user',
content: 'Tell me about the weather today.',
},
],
max_tokens: 150,
temperature: 0.8,
},
},
conversation: {
summary: 'Multi-turn Conversation',
description: 'Extended conversation with context',
value: {
model: LOCAL_MODELS[0],
messages: [
{
role: 'system',
content: 'You are a knowledgeable AI assistant.',
},
{
role: 'user',
content: 'What is machine learning?',
},
{
role: 'assistant',
content:
'Machine learning is a subset of artificial intelligence that enables systems to learn and improve from experience without being explicitly programmed.',
},
{
role: 'user',
content: 'Can you give me a simple example?',
},
],
max_tokens: 200,
temperature: 0.7,
},
},
streaming: {
summary: 'Streaming Chat',
description: 'Stream the response token by token',
value: {
model: LOCAL_MODELS[0],
messages: [
{
role: 'user',
content: 'Write a haiku about programming',
},
],
stream: true,
temperature: 0.9,
},
},
json_mode: {
summary: 'JSON Response',
description: 'Request structured JSON output',
value: {
model: LOCAL_MODELS[0],
messages: [
{
role: 'user',
content:
'List 3 programming languages with their main use cases in JSON format',
},
],
max_tokens: 200,
temperature: 0.5,
response_format: {
type: 'json_object',
},
},
},
},
},
},
},
responses: {
200: {
description: 'Successful Response',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/CreateChatCompletionResponse',
},
},
'text/event-stream': {
schema: {
type: 'string',
format: 'binary',
description: 'Server-sent events stream for streaming responses',
},
},
},
},
202: {
description: 'Accepted - Request is being processed',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/CreateChatCompletionResponse',
},
},
'text/event-stream': {
schema: {
type: 'string',
format: 'binary',
description: 'Server-sent events stream for streaming responses',
},
},
},
},
422: {
description: 'Validation Error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ValidationError',
},
},
},
},
},
},
}
// Add models endpoint
localSpec.paths['/v1/models'] = {
get: {
tags: ['Models'],
summary: 'List available models',
description:
'Lists the currently available models and provides basic information about each one such as the owner and availability.',
operationId: 'list_models',
responses: {
200: {
description: 'Successful Response',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ModelList',
},
example: {
object: 'list',
data: LOCAL_MODELS.map((id) => ({
id: id,
object: 'model',
created: 1686935002,
owned_by: 'jan',
})),
},
},
},
},
},
},
}
// Add tokenization endpoints
localSpec.paths['/extras/tokenize'] = {
post: {
tags: ['Extras'],
summary: 'Tokenize text',
description: "Convert text input into tokens using the model's tokenizer.",
operationId: 'tokenize',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/TokenizeRequest',
},
example: {
input: 'Hello, world!',
model: LOCAL_MODELS[0],
},
},
},
},
responses: {
200: {
description: 'Successful Response',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/TokenizeResponse',
},
example: {
tokens: [15339, 11, 1917, 0],
},
},
},
},
},
},
}
localSpec.paths['/extras/tokenize/count'] = {
post: {
tags: ['Extras'],
summary: 'Count tokens',
description: 'Count the number of tokens in the provided text.',
operationId: 'count_tokens',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/TokenizeRequest',
},
example: {
input: 'How many tokens does this text have?',
model: LOCAL_MODELS[0],
},
},
},
},
responses: {
200: {
description: 'Successful Response',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/TokenCountResponse',
},
example: {
count: 8,
},
},
},
},
},
},
}
// Copy ALL necessary schemas from cloud spec
const schemasToInclude = [
// Request/Response schemas
'CreateChatCompletionRequest',
'CreateChatCompletionResponse',
'CreateCompletionRequest',
'CreateCompletionResponse',
'ChatCompletionRequestMessage',
'ChatCompletionRequestSystemMessage',
'ChatCompletionRequestUserMessage',
'ChatCompletionRequestAssistantMessage',
'ChatCompletionResponseMessage',
'ChatCompletionResponseChoice',
'CompletionChoice',
'CompletionUsage',
'ModelList',
'ModelData',
'ValidationError',
// Additional message types
'ChatCompletionRequestFunctionMessage',
'ChatCompletionRequestToolMessage',
'ChatCompletionRequestMessageContentPart',
'ChatCompletionRequestMessageContentPartText',
'ChatCompletionRequestMessageContentPartImage',
// Function calling
'ChatCompletionFunction',
'ChatCompletionFunctionCall',
'ChatCompletionTool',
'ChatCompletionToolCall',
'ChatCompletionNamedToolChoice',
// Response format
'ChatCompletionRequestResponseFormat',
// Logprobs
'ChatCompletionLogprobs',
'ChatCompletionLogprobToken',
'ChatCompletionTopLogprobToken',
]
// Copy schemas from cloud spec (handle both definitions and schemas)
if (cloudSpec.definitions || cloudSpec.components?.schemas) {
const sourceSchemas =
cloudSpec.definitions || cloudSpec.components?.schemas || {}
schemasToInclude.forEach((schemaName) => {
if (sourceSchemas[schemaName]) {
localSpec.components.schemas[schemaName] = JSON.parse(
JSON.stringify(sourceSchemas[schemaName])
)
}
})
// Also copy any schemas that are referenced by the included schemas
const processedSchemas = new Set(schemasToInclude)
const schemasToProcess = [...schemasToInclude]
while (schemasToProcess.length > 0) {
const currentSchema = schemasToProcess.pop()
const schema = localSpec.components.schemas[currentSchema]
if (!schema) continue
// Find all $ref references
const schemaString = JSON.stringify(schema)
const refPattern = /#\/(?:definitions|components\/schemas)\/([^"]+)/g
let match
while ((match = refPattern.exec(schemaString)) !== null) {
const referencedSchema = match[1]
if (
!processedSchemas.has(referencedSchema) &&
sourceSchemas[referencedSchema]
) {
localSpec.components.schemas[referencedSchema] = JSON.parse(
JSON.stringify(sourceSchemas[referencedSchema])
)
processedSchemas.add(referencedSchema)
schemasToProcess.push(referencedSchema)
}
}
}
}
// Add tokenization schemas manually
localSpec.components.schemas.TokenizeRequest = {
type: 'object',
properties: {
input: {
type: 'string',
description: 'The text to tokenize',
},
model: {
type: 'string',
description: 'The model to use for tokenization',
enum: LOCAL_MODELS,
},
},
required: ['input'],
}
localSpec.components.schemas.TokenizeResponse = {
type: 'object',
properties: {
tokens: {
type: 'array',
items: {
type: 'integer',
},
description: 'Array of token IDs',
},
},
required: ['tokens'],
}
localSpec.components.schemas.TokenCountResponse = {
type: 'object',
properties: {
count: {
type: 'integer',
description: 'Number of tokens',
},
},
required: ['count'],
}
// Update model references in schemas to use local models
if (
localSpec.components.schemas.CreateChatCompletionRequest?.properties?.model
) {
localSpec.components.schemas.CreateChatCompletionRequest.properties.model = {
...localSpec.components.schemas.CreateChatCompletionRequest.properties
.model,
enum: LOCAL_MODELS,
example: LOCAL_MODELS[0],
description: `ID of the model to use. Available models: ${LOCAL_MODELS.join(', ')}`,
}
}
if (localSpec.components.schemas.CreateCompletionRequest?.properties?.model) {
localSpec.components.schemas.CreateCompletionRequest.properties.model = {
...localSpec.components.schemas.CreateCompletionRequest.properties.model,
enum: LOCAL_MODELS,
example: LOCAL_MODELS[0],
description: `ID of the model to use. Available models: ${LOCAL_MODELS.join(', ')}`,
}
}
// Fix all $ref references to use components/schemas instead of definitions
function fixReferences(obj) {
if (typeof obj === 'string') {
return obj.replace(/#\/definitions\//g, '#/components/schemas/')
}
if (Array.isArray(obj)) {
return obj.map(fixReferences)
}
if (obj && typeof obj === 'object') {
const fixed = {}
for (const key in obj) {
fixed[key] = fixReferences(obj[key])
}
return fixed
}
return obj
}
// Apply reference fixes
localSpec.paths = fixReferences(localSpec.paths)
localSpec.components.schemas = fixReferences(localSpec.components.schemas)
// Add x-jan-local-features
localSpec['x-jan-local-features'] = {
engine: 'llama.cpp',
features: [
'GGUF model support',
'CPU and GPU acceleration',
'Quantized model support (Q4, Q5, Q8)',
'Metal acceleration on macOS',
'CUDA support on NVIDIA GPUs',
'ROCm support on AMD GPUs',
'AVX/AVX2/AVX512 optimizations',
'Memory-mapped model loading',
],
privacy: {
local_processing: true,
no_telemetry: true,
offline_capable: true,
},
model_formats: ['GGUF', 'GGML'],
default_settings: {
context_length: 4096,
batch_size: 512,
threads: 'auto',
},
}
// Write the fixed spec
fs.writeFileSync(outputPath, JSON.stringify(localSpec, null, 2), 'utf8')
console.log('✅ Local OpenAPI spec fixed successfully!')
console.log(`📁 Output: ${outputPath}`)
console.log(`📊 Endpoints: ${Object.keys(localSpec.paths).length}`)
console.log(`📊 Schemas: ${Object.keys(localSpec.components.schemas).length}`)
console.log(
`🎯 Examples: ${Object.keys(localSpec.paths).reduce((count, path) => {
return (
count +
Object.keys(localSpec.paths[path]).reduce((c, method) => {
const examples =
localSpec.paths[path][method]?.requestBody?.content?.[
'application/json'
]?.examples
return c + (examples ? Object.keys(examples).length : 0)
}, 0)
)
}, 0)}`
)