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 { 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; }