import type { Recipe } from './types'; function safeNumber(value: unknown): number | undefined { if (value === null || value === undefined || value === '') return undefined; const num = typeof value === 'number' ? value : Number(value); return Number.isFinite(num) ? num : undefined;} export function slugifyRecipeId(input: string): string { return input .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '');} export function recipeToCommand(recipe: Recipe): string { const lines: string[] = []; const tp = recipe.tp || recipe.tensor_parallel_size || 1; const pp = recipe.pp || recipe.pipeline_parallel_size || 1; const totalGpus = tp * pp; if (totalGpus > 1) { const gpuIds = Array.from({ length: totalGpus }, (_, i) => i).join(','); lines.push(`CUDA_VISIBLE_DEVICES=${gpuIds} \\`); } const backend = recipe.backend || 'vllm'; if (backend === 'sglang') { lines.push(`python -m sglang.launch_server \\`); lines.push(` --model-path ${recipe.model_path} \\`); } else { lines.push(`vllm serve ${recipe.model_path} \\`); } const args: [string, string | number | boolean | undefined][] = [ ['--tensor-parallel-size', tp], ['--pipeline-parallel-size', pp > 1 ? pp : undefined], ['--dtype', recipe.dtype], ['--max-model-len', recipe.max_model_len], ['--block-size', safeNumber(recipe.block_size ?? (recipe.extra_args?.block_size as number | undefined))], ['--max-num-seqs', recipe.max_num_seqs], ['--max-num-batched-tokens', recipe.max_num_batched_tokens], ['--gpu-memory-utilization', recipe.gpu_memory_utilization], ['--swap-space', safeNumber(recipe.swap_space ?? (recipe.extra_args?.swap_space as number | undefined))], ['--kv-cache-dtype', recipe.kv_cache_dtype], ['--quantization', recipe.quantization], ['--reasoning-parser', (recipe.reasoning_parser ?? (recipe.extra_args?.reasoning_parser as string | undefined)) as string | undefined], ['--tool-call-parser', recipe.tool_call_parser], ['--served-model-name', recipe.served_model_name], ]; if (recipe.enable_auto_tool_choice) args.push(['--enable-auto-tool-choice', true]); if (recipe.disable_custom_all_reduce ?? recipe.extra_args?.disable_custom_all_reduce) args.push(['--disable-custom-all-reduce', true]); if (recipe.trust_remote_code ?? recipe.extra_args?.trust_remote_code) args.push(['--trust-remote-code', true]); if (recipe.disable_log_requests ?? recipe.extra_args?.disable_log_requests) args.push(['--disable-log-requests', true]); if (recipe.enable_expert_parallel ?? recipe.extra_args?.enable_expert_parallel) args.push(['--enable-expert-parallel', true]); for (const [flag, value] of args) { if (value === undefined || value === null || value === '') continue; if (typeof value === 'boolean') { if (value) lines.push(` ${flag} \\`); } else { lines.push(` ${flag} ${value} \\`); } } lines.push(` --host ${recipe.host || '0.0.0.0'} \\`); lines.push(` --port ${recipe.port || 8000}`); return lines.join('\n');} export function parseCommand(command: string, existingRecipe?: Partial<Recipe>): Recipe { const recipe: Recipe = { id: existingRecipe?.id || '', name: existingRecipe?.name || '', model_path: existingRecipe?.model_path || '', backend: 'vllm', extra_args: {}, }; const normalizedCmd = command.replace(/\\\s*\n/g, ' ').replace(/\s+/g, ' ').trim(); if (normalizedCmd.includes('sglang')) recipe.backend = 'sglang'; const modelMatch = normalizedCmd.match(/(?:vllm serve|--model-path|--model)\s+(\/[^\s]+)/); if (modelMatch) recipe.model_path = modelMatch[1]; const cudaMatch = normalizedCmd.match(/CUDA_VISIBLE_DEVICES=([\d,]+)/); if (cudaMatch) recipe.extra_args!.cuda_visible_devices = cudaMatch[1]; const flagPattern = /--([\w-]+)(?:\s+([^\s-][^\s]*))?/g; let match: RegExpExecArray | null; while ((match = flagPattern.exec(normalizedCmd)) !== null) { const [, flag, value] = match; switch (flag) { case 'tensor-parallel-size': recipe.tp = parseInt(value) || 1; recipe.tensor_parallel_size = recipe.tp; break; case 'pipeline-parallel-size': recipe.pp = parseInt(value) || 1; recipe.pipeline_parallel_size = recipe.pp; break; case 'max-model-len': recipe.max_model_len = parseInt(value) || undefined; break; case 'gpu-memory-utilization': recipe.gpu_memory_utilization = parseFloat(value) || 0.9; break; case 'max-num-seqs': recipe.max_num_seqs = parseInt(value) || undefined; break; case 'max-num-batched-tokens': recipe.max_num_batched_tokens = parseInt(value) || undefined; break; case 'kv-cache-dtype': recipe.kv_cache_dtype = value; break; case 'quantization': recipe.quantization = value; break; case 'dtype': recipe.dtype = value; break; case 'tool-call-parser': recipe.tool_call_parser = value; break; case 'served-model-name': recipe.served_model_name = value; break; case 'model': case 'model-path': if (!recipe.model_path && value) recipe.model_path = value; break; case 'port': recipe.port = parseInt(value) || 8000; break; case 'block-size': recipe.block_size = parseInt(value); break; case 'swap-space': recipe.swap_space = parseInt(value); break; case 'reasoning-parser': recipe.reasoning_parser = value; break; case 'enable-auto-tool-choice': recipe.enable_auto_tool_choice = true; break; case 'disable-custom-all-reduce': recipe.disable_custom_all_reduce = true; break; case 'trust-remote-code': recipe.trust_remote_code = true; break; case 'disable-log-requests': recipe.disable_log_requests = true; break; case 'enable-expert-parallel': recipe.enable_expert_parallel = true; break; } } if (!recipe.id && recipe.model_path) { recipe.id = slugifyRecipeId(recipe.model_path.split('/').pop() || 'new-recipe'); } if (!recipe.name && recipe.model_path) { recipe.name = recipe.model_path.split('/').pop() || 'New Recipe'; } return recipe;}
import type { Recipe } from './types';
function safeNumber(value: unknown): number | undefined {
if (value === null || value === undefined || value === '') return undefined;
const num = typeof value === 'number' ? value : Number(value);
return Number.isFinite(num) ? num : undefined;
}
export function slugifyRecipeId(input: string): string {
return input
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
export function recipeToCommand(recipe: Recipe): string {
const lines: string[] = [];
const tp = recipe.tp || recipe.tensor_parallel_size || 1;
const pp = recipe.pp || recipe.pipeline_parallel_size || 1;
const totalGpus = tp * pp;
if (totalGpus > 1) {
const gpuIds = Array.from({ length: totalGpus }, (_, i) => i).join(',');
lines.push(`CUDA_VISIBLE_DEVICES=${gpuIds} \\`);
}
const backend = recipe.backend || 'vllm';
if (backend === 'sglang') {
lines.push(`python -m sglang.launch_server \\`);
lines.push(` --model-path ${recipe.model_path} \\`);
} else {
lines.push(`vllm serve ${recipe.model_path} \\`);
}
const args: [string, string | number | boolean | undefined][] = [
['--tensor-parallel-size', tp],
['--pipeline-parallel-size', pp > 1 ? pp : undefined],
['--dtype', recipe.dtype],
['--max-model-len', recipe.max_model_len],
['--block-size', safeNumber(recipe.block_size ?? (recipe.extra_args?.block_size as number | undefined))],
['--max-num-seqs', recipe.max_num_seqs],
['--max-num-batched-tokens', recipe.max_num_batched_tokens],
['--gpu-memory-utilization', recipe.gpu_memory_utilization],
['--swap-space', safeNumber(recipe.swap_space ?? (recipe.extra_args?.swap_space as number | undefined))],
['--kv-cache-dtype', recipe.kv_cache_dtype],
['--quantization', recipe.quantization],
['--reasoning-parser', (recipe.reasoning_parser ?? (recipe.extra_args?.reasoning_parser as string | undefined)) as string | undefined],
['--tool-call-parser', recipe.tool_call_parser],
['--served-model-name', recipe.served_model_name],
];
if (recipe.enable_auto_tool_choice) args.push(['--enable-auto-tool-choice', true]);
if (recipe.disable_custom_all_reduce ?? recipe.extra_args?.disable_custom_all_reduce) args.push(['--disable-custom-all-reduce', true]);
if (recipe.trust_remote_code ?? recipe.extra_args?.trust_remote_code) args.push(['--trust-remote-code', true]);
if (recipe.disable_log_requests ?? recipe.extra_args?.disable_log_requests) args.push(['--disable-log-requests', true]);
if (recipe.enable_expert_parallel ?? recipe.extra_args?.enable_expert_parallel) args.push(['--enable-expert-parallel', true]);
for (const [flag, value] of args) {
if (value === undefined || value === null || value === '') continue;
if (typeof value === 'boolean') {
if (value) lines.push(` ${flag} \\`);
} else {
lines.push(` ${flag} ${value} \\`);
}
}
lines.push(` --host ${recipe.host || '0.0.0.0'} \\`);
lines.push(` --port ${recipe.port || 8000}`);
return lines.join('\n');
}
export function parseCommand(command: string, existingRecipe?: Partial<Recipe>): Recipe {
const recipe: Recipe = {
id: existingRecipe?.id || '',
name: existingRecipe?.name || '',
model_path: existingRecipe?.model_path || '',
backend: 'vllm',
extra_args: {},
};
const normalizedCmd = command.replace(/\\\s*\n/g, ' ').replace(/\s+/g, ' ').trim();
if (normalizedCmd.includes('sglang')) recipe.backend = 'sglang';
const modelMatch = normalizedCmd.match(/(?:vllm serve|--model-path|--model)\s+(\/[^\s]+)/);
if (modelMatch) recipe.model_path = modelMatch[1];
const cudaMatch = normalizedCmd.match(/CUDA_VISIBLE_DEVICES=([\d,]+)/);
if (cudaMatch) recipe.extra_args!.cuda_visible_devices = cudaMatch[1];
const flagPattern = /--([\w-]+)(?:\s+([^\s-][^\s]*))?/g;
let match: RegExpExecArray | null;
while ((match = flagPattern.exec(normalizedCmd)) !== null) {
const [, flag, value] = match;
switch (flag) {
case 'tensor-parallel-size':
recipe.tp = parseInt(value) || 1;
recipe.tensor_parallel_size = recipe.tp;
break;
case 'pipeline-parallel-size':
recipe.pp = parseInt(value) || 1;
recipe.pipeline_parallel_size = recipe.pp;
break;
case 'max-model-len':
recipe.max_model_len = parseInt(value) || undefined;
break;
case 'gpu-memory-utilization':
recipe.gpu_memory_utilization = parseFloat(value) || 0.9;
break;
case 'max-num-seqs':
recipe.max_num_seqs = parseInt(value) || undefined;
break;
case 'max-num-batched-tokens':
recipe.max_num_batched_tokens = parseInt(value) || undefined;
break;
case 'kv-cache-dtype':
recipe.kv_cache_dtype = value;
break;
case 'quantization':
recipe.quantization = value;
break;
case 'dtype':
recipe.dtype = value;
break;
case 'tool-call-parser':
recipe.tool_call_parser = value;
break;
case 'served-model-name':
recipe.served_model_name = value;
break;
case 'model':
case 'model-path':
if (!recipe.model_path && value) recipe.model_path = value;
break;
case 'port':
recipe.port = parseInt(value) || 8000;
break;
case 'block-size':
recipe.block_size = parseInt(value);
break;
case 'swap-space':
recipe.swap_space = parseInt(value);
break;
case 'reasoning-parser':
recipe.reasoning_parser = value;
break;
case 'enable-auto-tool-choice':
recipe.enable_auto_tool_choice = true;
break;
case 'disable-custom-all-reduce':
recipe.disable_custom_all_reduce = true;
break;
case 'trust-remote-code':
recipe.trust_remote_code = true;
break;
case 'disable-log-requests':
recipe.disable_log_requests = true;
break;
case 'enable-expert-parallel':
recipe.enable_expert_parallel = true;
break;
}
}
if (!recipe.id && recipe.model_path) {
recipe.id = slugifyRecipeId(recipe.model_path.split('/').pop() || 'new-recipe');
}
if (!recipe.name && recipe.model_path) {
recipe.name = recipe.model_path.split('/').pop() || 'New Recipe';
}
return recipe;
}