2026-07-19 18:08:36 +08:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""llama.cpp command generator - main Flask application."""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import json
|
|
|
|
|
|
import re
|
|
|
|
|
|
import math
|
2026-07-19 18:49:05 +08:00
|
|
|
|
import functools
|
2026-07-19 22:57:38 +08:00
|
|
|
|
import urllib.request
|
2026-07-19 18:49:05 +08:00
|
|
|
|
from flask import Flask, request, jsonify, send_from_directory, session, redirect
|
2026-07-19 18:08:36 +08:00
|
|
|
|
from flask_cors import CORS
|
|
|
|
|
|
from db import get_db, init_db, DB_PATH
|
|
|
|
|
|
|
|
|
|
|
|
app = Flask(__name__, static_folder='static', static_url_path='')
|
2026-07-19 18:49:05 +08:00
|
|
|
|
app.secret_key = 'llama-cmd-gen-secret-key-2026'
|
2026-07-19 18:08:36 +08:00
|
|
|
|
CORS(app)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 18:49:05 +08:00
|
|
|
|
# ==================== Admin Auth ====================
|
|
|
|
|
|
|
|
|
|
|
|
def admin_login_required(f):
|
|
|
|
|
|
@functools.wraps(f)
|
|
|
|
|
|
def wrapped(*args, **kwargs):
|
|
|
|
|
|
if not session.get('admin_logged_in'):
|
|
|
|
|
|
return jsonify({'error': '未登录或权限不足'}), 401
|
|
|
|
|
|
return f(*args, **kwargs)
|
|
|
|
|
|
return wrapped
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 18:08:36 +08:00
|
|
|
|
# ==================== Helper Functions ====================
|
|
|
|
|
|
|
|
|
|
|
|
def parse_param_value(param_type, value):
|
|
|
|
|
|
"""Parse parameter value based on its type."""
|
|
|
|
|
|
if value is None or value == '':
|
|
|
|
|
|
return None
|
|
|
|
|
|
if param_type == 'number':
|
|
|
|
|
|
try:
|
|
|
|
|
|
if '.' in str(value):
|
|
|
|
|
|
return float(value)
|
|
|
|
|
|
return int(value)
|
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
|
return value
|
|
|
|
|
|
elif param_type == 'boolean':
|
|
|
|
|
|
return str(value).lower() in ('true', '1', 'yes', 'on')
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_param_value(param_type, value):
|
|
|
|
|
|
"""Format parameter value for display."""
|
|
|
|
|
|
if param_type == 'number':
|
|
|
|
|
|
try:
|
|
|
|
|
|
f = float(value)
|
|
|
|
|
|
if f == int(f):
|
|
|
|
|
|
return str(int(f))
|
|
|
|
|
|
return str(f)
|
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
|
return str(value)
|
|
|
|
|
|
return str(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_flag_for_param(param):
|
|
|
|
|
|
"""Get the flag string for a parameter (prefers short flag)."""
|
|
|
|
|
|
if param['short_flag']:
|
|
|
|
|
|
return param['short_flag']
|
|
|
|
|
|
return param['long_flag']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_kv_cache_bytes_per_element(cache_type):
|
|
|
|
|
|
"""Get bytes per element for KV cache type."""
|
|
|
|
|
|
cache_type_map = {
|
|
|
|
|
|
'f32': 4,
|
|
|
|
|
|
'f16': 2,
|
|
|
|
|
|
'bf16': 2,
|
|
|
|
|
|
'q8_0': 1,
|
|
|
|
|
|
'q4_0': 0.5,
|
|
|
|
|
|
'q4_1': 0.5625,
|
|
|
|
|
|
'iq4_nl': 0.5,
|
|
|
|
|
|
'q5_0': 0.625,
|
|
|
|
|
|
'q5_1': 0.6875,
|
|
|
|
|
|
}
|
|
|
|
|
|
return cache_type_map.get(cache_type, 2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def estimate_vram(params_dict, gpus, version_params):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Estimate VRAM usage based on parameters.
|
|
|
|
|
|
Returns dict with breakdown.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Model parameters (user configurable hints)
|
|
|
|
|
|
model_size_gb = float(params_dict.get('_model_size_gb', 0) or 0)
|
|
|
|
|
|
model_layers = int(params_dict.get('_model_layers', 0) or 0)
|
|
|
|
|
|
model_embd = int(params_dict.get('_model_embd', 0) or 0)
|
|
|
|
|
|
model_heads = int(params_dict.get('_model_heads', 0) or 0)
|
|
|
|
|
|
model_kv_heads = int(params_dict.get('_model_kv_heads', 0) or 0)
|
|
|
|
|
|
model_head_dim = int(params_dict.get('_model_head_dim', 0) or 0)
|
|
|
|
|
|
|
|
|
|
|
|
# If model_layers not specified, try to estimate from model size
|
|
|
|
|
|
if model_layers == 0 and model_size_gb > 0:
|
|
|
|
|
|
# Rough estimate: ~0.3GB per layer for 7B, scale accordingly
|
|
|
|
|
|
model_layers = max(1, int(model_size_gb / 0.4))
|
|
|
|
|
|
|
|
|
|
|
|
# n_gpu_layers
|
|
|
|
|
|
ngl_raw = params_dict.get('n_gpu_layers', 'auto')
|
|
|
|
|
|
if ngl_raw in ('auto', 'all', '-1'):
|
|
|
|
|
|
ngl = model_layers if model_layers > 0 else 32
|
|
|
|
|
|
if ngl_raw == '0':
|
|
|
|
|
|
ngl = 0
|
|
|
|
|
|
else:
|
|
|
|
|
|
try:
|
|
|
|
|
|
ngl = int(ngl_raw)
|
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
|
ngl = 0
|
|
|
|
|
|
|
|
|
|
|
|
# Context size
|
|
|
|
|
|
ctx_size = int(params_dict.get('ctx_size', 0) or 0)
|
|
|
|
|
|
if ctx_size == 0:
|
|
|
|
|
|
ctx_size = 4096 # default assumption
|
|
|
|
|
|
|
|
|
|
|
|
# Batch size
|
|
|
|
|
|
batch_size = int(params_dict.get('batch_size', 2048) or 2048)
|
|
|
|
|
|
ubatch_size = int(params_dict.get('ubatch_size', 512) or 512)
|
|
|
|
|
|
|
|
|
|
|
|
# KV cache types
|
|
|
|
|
|
ctk = params_dict.get('cache_type_k', 'f16')
|
|
|
|
|
|
ctv = params_dict.get('cache_type_v', 'f16')
|
|
|
|
|
|
k_bytes = get_kv_cache_bytes_per_element(ctk)
|
|
|
|
|
|
v_bytes = get_kv_cache_bytes_per_element(ctv)
|
|
|
|
|
|
|
|
|
|
|
|
# Parallel slots
|
|
|
|
|
|
parallel = int(params_dict.get('parallel', 1) or 1)
|
|
|
|
|
|
if parallel < 1:
|
|
|
|
|
|
parallel = 1
|
|
|
|
|
|
|
|
|
|
|
|
# Flash attention
|
|
|
|
|
|
fa = params_dict.get('flash_attn', 'auto')
|
|
|
|
|
|
fa_enabled = fa in ('on', 'auto')
|
|
|
|
|
|
|
|
|
|
|
|
# === Calculate VRAM components ===
|
|
|
|
|
|
total_vram = sum(g['vram_mb'] for g in gpus) if gpus else 0
|
|
|
|
|
|
max_single_vram = max((g['vram_mb'] for g in gpus), default=0)
|
|
|
|
|
|
|
|
|
|
|
|
# 1. Model weights in VRAM
|
|
|
|
|
|
if model_size_gb > 0 and model_layers > 0:
|
|
|
|
|
|
ratio = min(1.0, ngl / model_layers) if ngl > 0 else 0
|
|
|
|
|
|
weights_vram_mb = model_size_gb * 1024 * ratio
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Fallback estimate: ~1GB per 1B params at Q4
|
|
|
|
|
|
weights_vram_mb = 0
|
|
|
|
|
|
|
|
|
|
|
|
# 2. KV cache
|
|
|
|
|
|
# KV cache per layer = 2 (K and V) * n_kv_heads * head_dim * ctx_size * bytes_per_element
|
|
|
|
|
|
if model_kv_heads > 0 and model_head_dim > 0:
|
|
|
|
|
|
kv_per_layer_bytes = 2 * model_kv_heads * model_head_dim * ctx_size
|
|
|
|
|
|
kv_total_bytes = kv_per_layer_bytes * model_layers * parallel
|
|
|
|
|
|
elif model_embd > 0:
|
|
|
|
|
|
# Fallback: use embedding dim
|
|
|
|
|
|
kv_per_layer_bytes = 2 * model_embd * ctx_size
|
|
|
|
|
|
kv_total_bytes = kv_per_layer_bytes * model_layers * parallel
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Very rough estimate: ~0.5MB per layer per 1K context at f16
|
|
|
|
|
|
kv_total_bytes = 0
|
|
|
|
|
|
if model_layers > 0:
|
|
|
|
|
|
kv_total_bytes = int(0.5 * 1024 * 1024 * model_layers * (ctx_size / 1024) * parallel)
|
|
|
|
|
|
|
|
|
|
|
|
kv_vram_mb = (kv_total_bytes * (k_bytes + v_bytes) / 2) / (1024 * 1024)
|
|
|
|
|
|
|
|
|
|
|
|
# If ngl < model_layers, only ngl layers' KV is on GPU
|
|
|
|
|
|
if model_layers > 0 and ngl < model_layers:
|
|
|
|
|
|
kv_vram_mb = kv_vram_mb * (ngl / model_layers)
|
|
|
|
|
|
|
|
|
|
|
|
# 3. Compute buffer / overhead
|
|
|
|
|
|
# Rough: batch_size * model_embd * 4 bytes * some factor
|
|
|
|
|
|
compute_mb = 0
|
|
|
|
|
|
if model_embd > 0:
|
|
|
|
|
|
compute_mb = (batch_size * model_embd * 4 * 2) / (1024 * 1024) # logits buffer
|
|
|
|
|
|
compute_mb = max(compute_mb, 100) # minimum overhead
|
|
|
|
|
|
|
|
|
|
|
|
# CUDA context overhead (~300-500MB per GPU)
|
|
|
|
|
|
cuda_overhead_mb = len(gpus) * 400 if gpus else 0
|
|
|
|
|
|
|
|
|
|
|
|
# Total
|
|
|
|
|
|
total_estimate = weights_vram_mb + kv_vram_mb + compute_mb + cuda_overhead_mb
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
'total_mb': round(total_estimate, 1),
|
|
|
|
|
|
'total_gb': round(total_estimate / 1024, 2),
|
|
|
|
|
|
'weights_mb': round(weights_vram_mb, 1),
|
|
|
|
|
|
'weights_gb': round(weights_vram_mb / 1024, 2),
|
|
|
|
|
|
'kv_cache_mb': round(kv_vram_mb, 1),
|
|
|
|
|
|
'kv_cache_gb': round(kv_vram_mb / 1024, 2),
|
|
|
|
|
|
'compute_mb': round(compute_mb, 1),
|
|
|
|
|
|
'cuda_overhead_mb': round(cuda_overhead_mb, 1),
|
|
|
|
|
|
'total_vram_available_mb': total_vram,
|
|
|
|
|
|
'total_vram_available_gb': round(total_vram / 1024, 2) if total_vram else 0,
|
|
|
|
|
|
'max_single_vram_mb': max_single_vram,
|
|
|
|
|
|
'fits': total_estimate <= total_vram if total_vram > 0 else None,
|
|
|
|
|
|
'usage_percent': round(total_estimate / total_vram * 100, 1) if total_vram > 0 else None,
|
|
|
|
|
|
# For CPU+GPU mode
|
|
|
|
|
|
'cpu_weights_mb': round(model_size_gb * 1024 * max(0, 1 - (ngl / model_layers if model_layers > 0 else 0)), 1) if model_size_gb > 0 else 0,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_natural_language(text):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Parse natural language description into parameter values.
|
|
|
|
|
|
Supports Chinese and English keywords.
|
|
|
|
|
|
"""
|
|
|
|
|
|
result = {}
|
|
|
|
|
|
text_lower = text.lower()
|
|
|
|
|
|
|
|
|
|
|
|
# GPU selection
|
|
|
|
|
|
gpu_patterns = [
|
|
|
|
|
|
(r'(?:rtx\s*)?3090', 'RTX 3090'),
|
|
|
|
|
|
(r'(?:rtx\s*)?4090', 'RTX 4090'),
|
|
|
|
|
|
(r'(?:rtx\s*)?4080', 'RTX 4080'),
|
|
|
|
|
|
(r'(?:rtx\s*)?3080', 'RTX 3080'),
|
|
|
|
|
|
(r'(?:rtx\s*)?5090', 'RTX 5090'),
|
|
|
|
|
|
(r'(?:rtx\s*)?4070\s*ti', 'RTX 4070 Ti'),
|
|
|
|
|
|
(r'(?:rtx\s*)?4060', 'RTX 4060'),
|
|
|
|
|
|
(r'a100\s*80', 'A100 80GB'),
|
|
|
|
|
|
(r'a100\s*40', 'A100 40GB'),
|
|
|
|
|
|
(r'h100', 'H100 80GB'),
|
|
|
|
|
|
(r'v100', 'V100 32GB'),
|
|
|
|
|
|
(r'a6000', 'RTX A6000'),
|
|
|
|
|
|
(r'a5000', 'RTX A5000'),
|
|
|
|
|
|
(r'7900\s*xtx', 'RX 7900 XTX'),
|
|
|
|
|
|
]
|
|
|
|
|
|
for pattern, gpu_name in gpu_patterns:
|
|
|
|
|
|
if re.search(pattern, text_lower):
|
|
|
|
|
|
result['_gpu_name'] = gpu_name
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# GPU count
|
|
|
|
|
|
gpu_count_match = re.search(r'(\d+)\s*(?:张|块|个)?\s*(?:gpu|显卡|卡)', text_lower)
|
|
|
|
|
|
if gpu_count_match:
|
|
|
|
|
|
result['_gpu_count'] = int(gpu_count_match.group(1))
|
|
|
|
|
|
|
|
|
|
|
|
# Context size
|
|
|
|
|
|
ctx_patterns = [
|
|
|
|
|
|
r'(?:上下文|context|ctx)[\s大小为]*[::\s]*(\d+)',
|
|
|
|
|
|
r'(\d+)\s*(?:k|K)\s*(?:上下文|context|ctx)',
|
|
|
|
|
|
]
|
|
|
|
|
|
for pattern in ctx_patterns:
|
|
|
|
|
|
m = re.search(pattern, text_lower)
|
|
|
|
|
|
if m:
|
|
|
|
|
|
val = int(m.group(1))
|
|
|
|
|
|
if val < 100: # like "8k context"
|
|
|
|
|
|
val = val * 1024
|
|
|
|
|
|
result['ctx_size'] = str(val)
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# GPU layers
|
|
|
|
|
|
ngl_patterns = [
|
|
|
|
|
|
r'(?:gpu\s*层|gpu\s*layers?|ngl|offload)[\s::]*(\d+)',
|
|
|
|
|
|
r'(\d+)\s*(?:层|layers?)\s*(?:gpu|offload)',
|
|
|
|
|
|
r'全部(?:offload|卸载|gpu)|all\s*(?:gpu|layers?)',
|
|
|
|
|
|
]
|
|
|
|
|
|
for pattern in ngl_patterns:
|
|
|
|
|
|
m = re.search(pattern, text_lower)
|
|
|
|
|
|
if m:
|
|
|
|
|
|
val = m.group(1) if m.lastindex else '-1'
|
|
|
|
|
|
result['n_gpu_layers'] = val
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# Temperature
|
|
|
|
|
|
temp_match = re.search(r'(?:温度|temp|temperature)[\s::]*(\d+(?:\.\d+)?)', text_lower)
|
|
|
|
|
|
if temp_match:
|
|
|
|
|
|
result['temperature'] = temp_match.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
# Top-k
|
|
|
|
|
|
topk_match = re.search(r'(?:top[\s-]*k)[\s::]*(\d+)', text_lower)
|
|
|
|
|
|
if topk_match:
|
|
|
|
|
|
result['top_k'] = topk_match.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
# Top-p
|
|
|
|
|
|
topp_match = re.search(r'(?:top[\s-]*p)[\s::]*(\d+(?:\.\d+)?)', text_lower)
|
|
|
|
|
|
if topp_match:
|
|
|
|
|
|
result['top_p'] = topp_match.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
# Threads
|
|
|
|
|
|
threads_match = re.search(r'(?:线程|threads?|cpu\s*线程)[\s::]*(\d+)', text_lower)
|
|
|
|
|
|
if threads_match:
|
|
|
|
|
|
result['threads'] = threads_match.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
# Batch size
|
|
|
|
|
|
batch_match = re.search(r'(?:batch|批处理|批次)[\s大小::]*(\d+)', text_lower)
|
|
|
|
|
|
if batch_match:
|
|
|
|
|
|
result['batch_size'] = batch_match.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
# Port
|
|
|
|
|
|
port_match = re.search(r'(?:端口|port)[\s::]*(\d+)', text_lower)
|
|
|
|
|
|
if port_match:
|
|
|
|
|
|
result['port'] = port_match.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
# Model path
|
|
|
|
|
|
model_match = re.search(r'(?:模型|model)[\s路径]*[::\s]+([^\s,,]+\.gguf)', text_lower)
|
|
|
|
|
|
if model_match:
|
|
|
|
|
|
result['model'] = model_match.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
# HF repo
|
|
|
|
|
|
hf_match = re.search(r'(?:hf|hugging\s*face|仓库)[\s::]+([^\s,,]+)', text_lower)
|
|
|
|
|
|
if hf_match and not model_match:
|
|
|
|
|
|
result['hf_repo'] = hf_match.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
# Flash attention
|
|
|
|
|
|
if re.search(r'flash\s*atten|flash\s*attn|fa', text_lower):
|
|
|
|
|
|
result['flash_attn'] = 'on'
|
|
|
|
|
|
|
|
|
|
|
|
# Mode
|
|
|
|
|
|
if re.search(r'cpu\s*\+\s*gpu|gpu\s*\+\s*cpu|混合', text_lower):
|
|
|
|
|
|
result['_mode'] = 'gpu_cpu'
|
|
|
|
|
|
elif re.search(r'纯\s*gpu|gpu\s*only|仅\s*gpu', text_lower):
|
|
|
|
|
|
result['_mode'] = 'gpu'
|
|
|
|
|
|
|
|
|
|
|
|
# Parallel
|
|
|
|
|
|
parallel_match = re.search(r'(?:并行|parallel|slots?)[\s::]*(\d+)', text_lower)
|
|
|
|
|
|
if parallel_match:
|
|
|
|
|
|
result['parallel'] = parallel_match.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
# Split mode
|
|
|
|
|
|
if re.search(r'row\s*split|行分割|按行分割', text_lower):
|
|
|
|
|
|
result['split_mode'] = 'row'
|
|
|
|
|
|
elif re.search(r'tensor\s*split|张量分割|按张量分割', text_lower):
|
|
|
|
|
|
result['split_mode'] = 'tensor'
|
|
|
|
|
|
|
|
|
|
|
|
# mlock
|
|
|
|
|
|
if re.search(r'mlock|锁[\s定]*内存|内存锁', text_lower):
|
|
|
|
|
|
result['mlock'] = 'true'
|
|
|
|
|
|
|
|
|
|
|
|
# numa
|
|
|
|
|
|
numa_match = re.search(r'numa[\s::]*(distribute|isolate|numactl)', text_lower)
|
|
|
|
|
|
if numa_match:
|
|
|
|
|
|
result['numa'] = numa_match.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
# Reasoning budget
|
|
|
|
|
|
reasoning_match = re.search(r'(?:reasoning|推理|thinking)[\s预算budget]*[::\s]*(\d+)', text_lower)
|
|
|
|
|
|
if reasoning_match:
|
|
|
|
|
|
result['reasoning_budget'] = reasoning_match.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== API Routes ====================
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/')
|
|
|
|
|
|
def index():
|
|
|
|
|
|
return send_from_directory('static', 'index.html')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/admin')
|
|
|
|
|
|
def admin():
|
2026-07-19 18:49:05 +08:00
|
|
|
|
# Serve admin page; JS handles login check
|
2026-07-19 18:08:36 +08:00
|
|
|
|
return send_from_directory('static', 'admin.html')
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 18:49:05 +08:00
|
|
|
|
# ----- Admin Login/Logout -----
|
|
|
|
|
|
@app.route('/api/admin/login', methods=['POST'])
|
|
|
|
|
|
def admin_login():
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
password = data.get('password', '')
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
setting = db.execute('SELECT value FROM settings WHERE key = ?', ('admin_password',)).fetchone()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
stored_password = setting['value'] if setting else 'admin123'
|
|
|
|
|
|
if password == stored_password:
|
|
|
|
|
|
session['admin_logged_in'] = True
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
return jsonify({'error': '密码错误'}), 401
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/admin/logout', methods=['POST'])
|
|
|
|
|
|
def admin_logout():
|
|
|
|
|
|
session.pop('admin_logged_in', None)
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/admin/check')
|
|
|
|
|
|
def admin_check():
|
|
|
|
|
|
return jsonify({'logged_in': session.get('admin_logged_in', False)})
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 18:08:36 +08:00
|
|
|
|
# ----- Versions -----
|
|
|
|
|
|
@app.route('/api/versions')
|
|
|
|
|
|
def get_versions():
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
versions = db.execute('SELECT * FROM llama_versions WHERE is_active = 1 ORDER BY sort_order').fetchall()
|
|
|
|
|
|
result = [dict(v) for v in versions]
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/versions/<int:vid>/params')
|
|
|
|
|
|
def get_version_params(vid):
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
params = db.execute(
|
|
|
|
|
|
'SELECT * FROM params WHERE version_id = ? ORDER BY is_important DESC, sort_order',
|
|
|
|
|
|
(vid,)
|
|
|
|
|
|
).fetchall()
|
|
|
|
|
|
result = []
|
|
|
|
|
|
for p in params:
|
|
|
|
|
|
d = dict(p)
|
|
|
|
|
|
if d.get('options'):
|
|
|
|
|
|
try:
|
|
|
|
|
|
d['options'] = json.loads(d['options'])
|
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
result.append(d)
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ----- GPUs -----
|
|
|
|
|
|
@app.route('/api/gpus')
|
|
|
|
|
|
def get_gpus():
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
gpus = db.execute('SELECT * FROM gpus ORDER BY sort_order, name').fetchall()
|
|
|
|
|
|
result = [dict(g) for g in gpus]
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ----- Generate Command -----
|
|
|
|
|
|
@app.route('/api/generate', methods=['POST'])
|
|
|
|
|
|
def generate_command():
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
version_id = data.get('version_id')
|
|
|
|
|
|
params = data.get('params', {})
|
|
|
|
|
|
mode = data.get('mode', 'gpu')
|
|
|
|
|
|
gpu_selections = data.get('gpu_selections', [])
|
|
|
|
|
|
binary = data.get('binary', 'llama-server')
|
|
|
|
|
|
system_memory_gb = data.get('system_memory_gb', 0)
|
|
|
|
|
|
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
|
|
|
|
|
|
# Get param definitions
|
|
|
|
|
|
param_defs = db.execute(
|
|
|
|
|
|
'SELECT * FROM params WHERE version_id = ?',
|
|
|
|
|
|
(version_id,)
|
|
|
|
|
|
).fetchall()
|
|
|
|
|
|
param_def_map = {p['param_key']: dict(p) for p in param_defs}
|
|
|
|
|
|
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
|
|
|
|
|
|
# Build command
|
|
|
|
|
|
cmd_parts = [binary]
|
|
|
|
|
|
|
|
|
|
|
|
# GPU layers
|
|
|
|
|
|
if mode == 'gpu':
|
|
|
|
|
|
# Add GPU-related args
|
|
|
|
|
|
if gpu_selections:
|
|
|
|
|
|
gpu_names = [g.get('name', '') for g in gpu_selections]
|
|
|
|
|
|
device_str = ','.join(str(i) for i in range(len(gpu_selections)))
|
|
|
|
|
|
|
|
|
|
|
|
# n_gpu_layers
|
|
|
|
|
|
ngl = params.get('n_gpu_layers', 'auto')
|
|
|
|
|
|
if ngl and ngl != param_def_map.get('n_gpu_layers', {}).get('default_value', 'auto'):
|
|
|
|
|
|
flag = param_def_map.get('n_gpu_layers', {}).get('short_flag') or param_def_map.get('n_gpu_layers', {}).get('long_flag', '-ngl')
|
|
|
|
|
|
cmd_parts.append(f'{flag} {ngl}')
|
|
|
|
|
|
|
|
|
|
|
|
# tensor_split for multiple GPUs
|
|
|
|
|
|
if len(gpu_selections) > 1:
|
|
|
|
|
|
ts = params.get('tensor_split', '')
|
|
|
|
|
|
if ts and ts != param_def_map.get('tensor_split', {}).get('default_value', ''):
|
|
|
|
|
|
flag = param_def_map.get('tensor_split', {}).get('short_flag', '-ts')
|
|
|
|
|
|
cmd_parts.append(f'{flag} {ts}')
|
|
|
|
|
|
elif not ts:
|
|
|
|
|
|
# Auto-generate tensor split based on VRAM ratio
|
|
|
|
|
|
total_vram = sum(g.get('vram_mb', 0) for g in gpu_selections)
|
|
|
|
|
|
if total_vram > 0:
|
|
|
|
|
|
ratios = [str(round(g.get('vram_mb', 0) / total_vram, 2)) for g in gpu_selections]
|
|
|
|
|
|
cmd_parts.append(f'-ts {",".join(ratios)}')
|
|
|
|
|
|
|
|
|
|
|
|
elif mode == 'gpu_cpu':
|
|
|
|
|
|
# GPU+CPU mode
|
|
|
|
|
|
if gpu_selections:
|
|
|
|
|
|
ngl = params.get('n_gpu_layers', 'auto')
|
|
|
|
|
|
if ngl and ngl != param_def_map.get('n_gpu_layers', {}).get('default_value', 'auto'):
|
|
|
|
|
|
flag = param_def_map.get('n_gpu_layers', {}).get('short_flag') or param_def_map.get('n_gpu_layers', {}).get('long_flag', '-ngl')
|
|
|
|
|
|
cmd_parts.append(f'{flag} {ngl}')
|
|
|
|
|
|
|
|
|
|
|
|
# Iterate through params and add non-default ones
|
|
|
|
|
|
skip_params = {'n_gpu_layers', 'tensor_split'}
|
|
|
|
|
|
for key, value in params.items():
|
|
|
|
|
|
if key.startswith('_'):
|
|
|
|
|
|
continue
|
|
|
|
|
|
if key in skip_params:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
p_def = param_def_map.get(key)
|
|
|
|
|
|
if not p_def:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
default_val = p_def['default_value']
|
|
|
|
|
|
# Skip if value equals default
|
|
|
|
|
|
if str(value) == str(default_val):
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# Skip empty values
|
|
|
|
|
|
if value is None or value == '' or value == 'false':
|
|
|
|
|
|
if str(default_val).lower() == 'false' and str(value).lower() == 'false':
|
|
|
|
|
|
continue
|
|
|
|
|
|
if value == '' or value is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# Boolean params: only add if true (and default is false)
|
|
|
|
|
|
if p_def['param_type'] == 'boolean':
|
|
|
|
|
|
if str(value).lower() == 'true' and str(default_val).lower() != 'true':
|
|
|
|
|
|
flag = p_def['short_flag'] or p_def['long_flag']
|
|
|
|
|
|
cmd_parts.append(flag)
|
|
|
|
|
|
elif str(value).lower() == 'false' and str(default_val).lower() == 'true':
|
|
|
|
|
|
# Add --no- variant
|
|
|
|
|
|
flag = p_def['long_flag']
|
|
|
|
|
|
cmd_parts.append(f'--no-{flag.lstrip("--")}')
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
flag = p_def['short_flag'] or p_def['long_flag']
|
|
|
|
|
|
cmd_parts.append(f'{flag} {value}')
|
|
|
|
|
|
|
|
|
|
|
|
command = ' '.join(cmd_parts)
|
|
|
|
|
|
return jsonify({'command': command, 'mode': mode, 'binary': binary})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ----- Estimate VRAM -----
|
|
|
|
|
|
@app.route('/api/estimate', methods=['POST'])
|
|
|
|
|
|
def estimate():
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
params = data.get('params', {})
|
|
|
|
|
|
gpu_selections = data.get('gpu_selections', [])
|
|
|
|
|
|
mode = data.get('mode', 'gpu')
|
|
|
|
|
|
system_memory_gb = data.get('system_memory_gb', 0)
|
|
|
|
|
|
|
|
|
|
|
|
# Prepare GPU list with VRAM
|
|
|
|
|
|
gpus = []
|
|
|
|
|
|
for gs in gpu_selections:
|
|
|
|
|
|
gpus.append({'vram_mb': gs.get('vram_mb', 0), 'name': gs.get('name', '')})
|
|
|
|
|
|
|
|
|
|
|
|
result = estimate_vram(params, gpus, [])
|
|
|
|
|
|
|
|
|
|
|
|
# Add system memory estimation for GPU+CPU mode
|
|
|
|
|
|
if mode == 'gpu_cpu':
|
|
|
|
|
|
model_size_gb = float(params.get('_model_size_gb', 0) or 0)
|
|
|
|
|
|
model_layers = int(params.get('_model_layers', 0) or 0)
|
|
|
|
|
|
ngl_raw = params.get('n_gpu_layers', 'auto')
|
|
|
|
|
|
|
|
|
|
|
|
if ngl_raw in ('auto', 'all', '-1'):
|
|
|
|
|
|
ngl = model_layers if model_layers > 0 else 32
|
|
|
|
|
|
else:
|
|
|
|
|
|
try:
|
|
|
|
|
|
ngl = int(ngl_raw)
|
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
|
ngl = 0
|
|
|
|
|
|
|
|
|
|
|
|
# CPU portion of weights
|
|
|
|
|
|
if model_layers > 0 and ngl < model_layers:
|
|
|
|
|
|
cpu_weights_gb = model_size_gb * (1 - ngl / model_layers)
|
|
|
|
|
|
else:
|
|
|
|
|
|
cpu_weights_gb = 0
|
|
|
|
|
|
|
|
|
|
|
|
# KV cache on CPU
|
|
|
|
|
|
ctx_size = int(params.get('ctx_size', 0) or 0)
|
|
|
|
|
|
if ctx_size == 0:
|
|
|
|
|
|
ctx_size = 4096
|
|
|
|
|
|
parallel = int(params.get('parallel', 1) or 1)
|
|
|
|
|
|
if parallel < 1:
|
|
|
|
|
|
parallel = 1
|
|
|
|
|
|
|
|
|
|
|
|
# Estimate KV cache on CPU (layers not on GPU)
|
|
|
|
|
|
model_kv_heads = int(params.get('_model_kv_heads', 0) or 0)
|
|
|
|
|
|
model_head_dim = int(params.get('_model_head_dim', 0) or 0)
|
|
|
|
|
|
model_embd = int(params.get('_model_embd', 0) or 0)
|
|
|
|
|
|
|
|
|
|
|
|
if model_layers > 0 and ngl < model_layers:
|
|
|
|
|
|
remaining_layers = model_layers - ngl
|
|
|
|
|
|
if model_kv_heads > 0 and model_head_dim > 0:
|
|
|
|
|
|
kv_cpu_bytes = 2 * model_kv_heads * model_head_dim * ctx_size * remaining_layers * parallel
|
|
|
|
|
|
elif model_embd > 0:
|
|
|
|
|
|
kv_cpu_bytes = 2 * model_embd * ctx_size * remaining_layers * parallel
|
|
|
|
|
|
else:
|
|
|
|
|
|
kv_cpu_bytes = int(0.5 * 1024 * 1024 * remaining_layers * (ctx_size / 1024) * parallel) * 2
|
|
|
|
|
|
kv_cpu_mb = kv_cpu_bytes * 2 / (1024 * 1024) # Assuming f16
|
|
|
|
|
|
else:
|
|
|
|
|
|
kv_cpu_mb = 0
|
|
|
|
|
|
|
|
|
|
|
|
total_cpu_mb = cpu_weights_gb * 1024 + kv_cpu_mb + 500 # overhead
|
|
|
|
|
|
result['cpu_total_mb'] = round(total_cpu_mb, 1)
|
|
|
|
|
|
result['cpu_total_gb'] = round(total_cpu_mb / 1024, 2)
|
|
|
|
|
|
result['cpu_weights_gb'] = round(cpu_weights_gb, 2)
|
|
|
|
|
|
result['cpu_kv_cache_mb'] = round(kv_cpu_mb, 1)
|
|
|
|
|
|
result['system_memory_gb'] = system_memory_gb
|
|
|
|
|
|
if system_memory_gb > 0:
|
|
|
|
|
|
result['cpu_usage_percent'] = round(total_cpu_mb / (system_memory_gb * 1024) * 100, 1)
|
|
|
|
|
|
|
|
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 18:49:05 +08:00
|
|
|
|
# ----- Models (public read) -----
|
|
|
|
|
|
@app.route('/api/models')
|
|
|
|
|
|
def get_models():
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
models = db.execute('SELECT * FROM models ORDER BY sort_order, name').fetchall()
|
|
|
|
|
|
result = [dict(m) for m in models]
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
|
2026-07-19 22:57:38 +08:00
|
|
|
|
@app.route('/api/models/grouped')
|
|
|
|
|
|
def get_models_grouped():
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
models = db.execute('SELECT * FROM models ORDER BY base_model, sort_order, name').fetchall()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
grouped = {}
|
|
|
|
|
|
for m in models:
|
|
|
|
|
|
d = dict(m)
|
|
|
|
|
|
base = d['base_model']
|
|
|
|
|
|
if base not in grouped:
|
|
|
|
|
|
grouped[base] = []
|
|
|
|
|
|
grouped[base].append(d)
|
|
|
|
|
|
# Get default quant from settings
|
|
|
|
|
|
db2 = get_db()
|
|
|
|
|
|
dq = db2.execute("SELECT value FROM settings WHERE key = 'default_quant'").fetchone()
|
|
|
|
|
|
db2.close()
|
|
|
|
|
|
default_quant = dq['value'] if dq else 'Q4_K_M'
|
|
|
|
|
|
return jsonify({'models': grouped, 'default_quant': default_quant})
|
|
|
|
|
|
|
2026-07-19 18:49:05 +08:00
|
|
|
|
|
2026-07-19 18:08:36 +08:00
|
|
|
|
# ----- Parse Natural Language -----
|
|
|
|
|
|
@app.route('/api/parse-nl', methods=['POST'])
|
|
|
|
|
|
def parse_nl():
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
text = data.get('text', '')
|
2026-07-19 22:57:38 +08:00
|
|
|
|
# Try LLM API first if enabled
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
llm_enabled = db.execute("SELECT value FROM settings WHERE key = 'llm_enabled'").fetchone()
|
|
|
|
|
|
if llm_enabled and llm_enabled['value'] == 'true':
|
|
|
|
|
|
llm_url = db.execute("SELECT value FROM settings WHERE key = 'llm_api_url'").fetchone()
|
|
|
|
|
|
llm_key = db.execute("SELECT value FROM settings WHERE key = 'llm_api_key'").fetchone()
|
|
|
|
|
|
llm_model = db.execute("SELECT value FROM settings WHERE key = 'llm_api_model'").fetchone()
|
|
|
|
|
|
llm_prompt = db.execute("SELECT value FROM settings WHERE key = 'llm_system_prompt'").fetchone()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
url = llm_url['value'] if llm_url else ''
|
|
|
|
|
|
key = llm_key['value'] if llm_key else ''
|
|
|
|
|
|
model = llm_model['value'] if llm_model else ''
|
|
|
|
|
|
system_prompt = llm_prompt['value'] if llm_prompt else ''
|
|
|
|
|
|
if url:
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = call_llm_for_parsing(url, key, model, system_prompt, text)
|
|
|
|
|
|
if result:
|
|
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f'LLM parse failed: {e}', file=sys.stderr)
|
|
|
|
|
|
else:
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
# Fallback to regex parsing
|
2026-07-19 18:08:36 +08:00
|
|
|
|
result = parse_natural_language(text)
|
|
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 22:57:38 +08:00
|
|
|
|
def call_llm_for_parsing(url, key, model, system_prompt, user_text):
|
|
|
|
|
|
"""Call LLM API to parse natural language into params."""
|
|
|
|
|
|
headers = {'Content-Type': 'application/json'}
|
|
|
|
|
|
if key:
|
|
|
|
|
|
headers['Authorization'] = f'Bearer {key}'
|
|
|
|
|
|
body = {
|
|
|
|
|
|
'model': model,
|
|
|
|
|
|
'messages': [
|
|
|
|
|
|
{'role': 'system', 'content': system_prompt},
|
|
|
|
|
|
{'role': 'user', 'content': user_text}
|
|
|
|
|
|
],
|
|
|
|
|
|
'temperature': 0.1,
|
|
|
|
|
|
'max_tokens': 2000,
|
|
|
|
|
|
}
|
|
|
|
|
|
req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method='POST')
|
|
|
|
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
|
|
|
|
data = json.loads(resp.read().decode('utf-8'))
|
|
|
|
|
|
# OpenAI-compatible response
|
|
|
|
|
|
content = data['choices'][0]['message']['content']
|
|
|
|
|
|
# Try to extract JSON from the response
|
|
|
|
|
|
content = content.strip()
|
|
|
|
|
|
if content.startswith('```'):
|
|
|
|
|
|
content = re.sub(r'^```\w*\n?', '', content)
|
|
|
|
|
|
content = re.sub(r'\n?```$', '', content)
|
|
|
|
|
|
result = json.loads(content)
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 18:08:36 +08:00
|
|
|
|
# ==================== Admin API ====================
|
|
|
|
|
|
|
2026-07-19 18:49:05 +08:00
|
|
|
|
# All admin routes below require login
|
|
|
|
|
|
|
2026-07-19 18:08:36 +08:00
|
|
|
|
@app.route('/api/admin/gpus', methods=['GET', 'POST'])
|
2026-07-19 18:49:05 +08:00
|
|
|
|
@admin_login_required
|
2026-07-19 18:08:36 +08:00
|
|
|
|
def admin_gpus():
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
if request.method == 'GET':
|
|
|
|
|
|
gpus = db.execute('SELECT * FROM gpus ORDER BY sort_order, name').fetchall()
|
|
|
|
|
|
result = [dict(g) for g in gpus]
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
|
|
|
|
|
|
elif request.method == 'POST':
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
db.execute(
|
|
|
|
|
|
'INSERT INTO gpus (name, vram_mb, compute_capability, description, sort_order) VALUES (?, ?, ?, ?, ?)',
|
|
|
|
|
|
(data['name'], data['vram_mb'], data.get('compute_capability', ''),
|
|
|
|
|
|
data.get('description', ''), data.get('sort_order', 0))
|
|
|
|
|
|
)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/admin/gpus/<int:gid>', methods=['PUT', 'DELETE'])
|
2026-07-19 18:49:05 +08:00
|
|
|
|
@admin_login_required
|
2026-07-19 18:08:36 +08:00
|
|
|
|
def admin_gpu_edit(gid):
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
if request.method == 'PUT':
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
db.execute(
|
|
|
|
|
|
'UPDATE gpus SET name=?, vram_mb=?, compute_capability=?, description=?, sort_order=? WHERE id=?',
|
|
|
|
|
|
(data['name'], data['vram_mb'], data.get('compute_capability', ''),
|
|
|
|
|
|
data.get('description', ''), data.get('sort_order', 0), gid)
|
|
|
|
|
|
)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
elif request.method == 'DELETE':
|
|
|
|
|
|
db.execute('DELETE FROM gpus WHERE id=?', (gid,))
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/admin/versions', methods=['GET', 'POST'])
|
2026-07-19 18:49:05 +08:00
|
|
|
|
@admin_login_required
|
2026-07-19 18:08:36 +08:00
|
|
|
|
def admin_versions():
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
if request.method == 'GET':
|
|
|
|
|
|
versions = db.execute('SELECT * FROM llama_versions ORDER BY sort_order').fetchall()
|
|
|
|
|
|
result = [dict(v) for v in versions]
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
elif request.method == 'POST':
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
db.execute(
|
|
|
|
|
|
'INSERT INTO llama_versions (version_tag, description, release_date, is_active, sort_order) VALUES (?, ?, ?, ?, ?)',
|
|
|
|
|
|
(data['version_tag'], data.get('description', ''), data.get('release_date', ''),
|
|
|
|
|
|
data.get('is_active', 1), data.get('sort_order', 0))
|
|
|
|
|
|
)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/admin/versions/<int:vid>', methods=['PUT', 'DELETE'])
|
2026-07-19 18:49:05 +08:00
|
|
|
|
@admin_login_required
|
2026-07-19 18:08:36 +08:00
|
|
|
|
def admin_version_edit(vid):
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
if request.method == 'PUT':
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
db.execute(
|
|
|
|
|
|
'UPDATE llama_versions SET version_tag=?, description=?, release_date=?, is_active=?, sort_order=? WHERE id=?',
|
|
|
|
|
|
(data['version_tag'], data.get('description', ''), data.get('release_date', ''),
|
|
|
|
|
|
data.get('is_active', 1), data.get('sort_order', 0), vid)
|
|
|
|
|
|
)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
elif request.method == 'DELETE':
|
|
|
|
|
|
db.execute('DELETE FROM llama_versions WHERE id=?', (vid,))
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/admin/versions/<int:vid>/params', methods=['GET', 'POST'])
|
2026-07-19 18:49:05 +08:00
|
|
|
|
@admin_login_required
|
2026-07-19 18:08:36 +08:00
|
|
|
|
def admin_params(vid):
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
if request.method == 'GET':
|
|
|
|
|
|
params = db.execute(
|
|
|
|
|
|
'SELECT * FROM params WHERE version_id = ? ORDER BY is_important DESC, sort_order',
|
|
|
|
|
|
(vid,)
|
|
|
|
|
|
).fetchall()
|
|
|
|
|
|
result = []
|
|
|
|
|
|
for p in params:
|
|
|
|
|
|
d = dict(p)
|
|
|
|
|
|
if d.get('options'):
|
|
|
|
|
|
try:
|
|
|
|
|
|
d['options'] = json.loads(d['options'])
|
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
result.append(d)
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
elif request.method == 'POST':
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
options = data.get('options')
|
|
|
|
|
|
if isinstance(options, list):
|
|
|
|
|
|
options = json.dumps(options)
|
|
|
|
|
|
db.execute(
|
|
|
|
|
|
'''INSERT INTO params
|
|
|
|
|
|
(version_id, param_key, short_flag, long_flag, description, category, param_type,
|
|
|
|
|
|
default_value, options, min_value, max_value, step, unit, is_important, affects_vram, sort_order)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
|
|
|
|
|
(vid, data['param_key'], data.get('short_flag', ''), data['long_flag'],
|
|
|
|
|
|
data.get('description', ''), data.get('category', 'common'),
|
|
|
|
|
|
data.get('param_type', 'string'), data.get('default_value', ''),
|
|
|
|
|
|
options, data.get('min_value'), data.get('max_value'),
|
|
|
|
|
|
data.get('step'), data.get('unit'), data.get('is_important', 0),
|
|
|
|
|
|
data.get('affects_vram', 0), data.get('sort_order', 0))
|
|
|
|
|
|
)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/admin/params/<int:pid>', methods=['PUT', 'DELETE'])
|
2026-07-19 18:49:05 +08:00
|
|
|
|
@admin_login_required
|
2026-07-19 18:08:36 +08:00
|
|
|
|
def admin_param_edit(pid):
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
if request.method == 'PUT':
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
options = data.get('options')
|
|
|
|
|
|
if isinstance(options, list):
|
|
|
|
|
|
options = json.dumps(options)
|
|
|
|
|
|
db.execute(
|
|
|
|
|
|
'''UPDATE params SET
|
|
|
|
|
|
param_key=?, short_flag=?, long_flag=?, description=?, category=?, param_type=?,
|
|
|
|
|
|
default_value=?, options=?, min_value=?, max_value=?, step=?, unit=?,
|
|
|
|
|
|
is_important=?, affects_vram=?, sort_order=? WHERE id=?''',
|
|
|
|
|
|
(data['param_key'], data.get('short_flag', ''), data['long_flag'],
|
|
|
|
|
|
data.get('description', ''), data.get('category', 'common'),
|
|
|
|
|
|
data.get('param_type', 'string'), data.get('default_value', ''),
|
|
|
|
|
|
options, data.get('min_value'), data.get('max_value'),
|
|
|
|
|
|
data.get('step'), data.get('unit'), data.get('is_important', 0),
|
|
|
|
|
|
data.get('affects_vram', 0), data.get('sort_order', 0), pid)
|
|
|
|
|
|
)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
elif request.method == 'DELETE':
|
|
|
|
|
|
db.execute('DELETE FROM params WHERE id=?', (pid,))
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/admin/settings', methods=['GET', 'PUT'])
|
2026-07-19 18:49:05 +08:00
|
|
|
|
@admin_login_required
|
2026-07-19 18:08:36 +08:00
|
|
|
|
def admin_settings():
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
if request.method == 'GET':
|
|
|
|
|
|
settings = db.execute('SELECT * FROM settings').fetchall()
|
|
|
|
|
|
result = {s['key']: s['value'] for s in settings}
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
elif request.method == 'PUT':
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
for key, value in data.items():
|
|
|
|
|
|
db.execute(
|
|
|
|
|
|
'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)',
|
|
|
|
|
|
(key, str(value))
|
|
|
|
|
|
)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 18:49:05 +08:00
|
|
|
|
# ----- Admin Models CRUD -----
|
|
|
|
|
|
@app.route('/api/admin/models', methods=['POST'])
|
|
|
|
|
|
@admin_login_required
|
|
|
|
|
|
def admin_add_model():
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
db.execute(
|
2026-07-19 22:57:38 +08:00
|
|
|
|
'''INSERT INTO models (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, quant, description, sort_order)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
|
|
|
|
|
(data['base_model'], data['name'], data['size_gb'], data['layers'], data['embd'],
|
2026-07-19 18:49:05 +08:00
|
|
|
|
data['kv_heads'], data['head_dim'], data['attention_heads'],
|
|
|
|
|
|
data.get('quant', ''), data.get('description', ''), data.get('sort_order', 0))
|
|
|
|
|
|
)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/admin/models/<int:mid>', methods=['PUT', 'DELETE'])
|
|
|
|
|
|
@admin_login_required
|
|
|
|
|
|
def admin_model_edit(mid):
|
|
|
|
|
|
db = get_db()
|
|
|
|
|
|
if request.method == 'PUT':
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
db.execute(
|
2026-07-19 22:57:38 +08:00
|
|
|
|
'''UPDATE models SET base_model=?, name=?, size_gb=?, layers=?, embd=?, kv_heads=?,
|
2026-07-19 18:49:05 +08:00
|
|
|
|
head_dim=?, attention_heads=?, quant=?, description=?, sort_order=? WHERE id=?''',
|
2026-07-19 22:57:38 +08:00
|
|
|
|
(data['base_model'], data['name'], data['size_gb'], data['layers'], data['embd'],
|
2026-07-19 18:49:05 +08:00
|
|
|
|
data['kv_heads'], data['head_dim'], data['attention_heads'],
|
|
|
|
|
|
data.get('quant', ''), data.get('description', ''), data.get('sort_order', 0), mid)
|
|
|
|
|
|
)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
elif request.method == 'DELETE':
|
|
|
|
|
|
db.execute('DELETE FROM models WHERE id=?', (mid,))
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 18:08:36 +08:00
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
init_db()
|
|
|
|
|
|
app.run(host='0.0.0.0', port=16052, debug=False)
|