747 lines
21 KiB
JavaScript
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)}`
|
|
)
|