Commit 3716e54b authored by Stefy Lanza (nextime / spora )'s avatar Stefy Lanza (nextime / spora )

Merge branch 'experimental'

parents 52316b8b ffd12aa6
......@@ -128,6 +128,22 @@ source venv_all/bin/activate
python coderai # starts on http://127.0.0.1:8776
```
macOS:
```bash
./osxbuild.sh all
source venv_osx_all/bin/activate
python coderai
```
Windows PowerShell:
```powershell
.\build.ps1 -Backend all
.\venv_win_all\Scripts\Activate.ps1
python coderai
```
That's it. Open `http://127.0.0.1:8776/admin` and log in with `admin` / `admin`.
---
......@@ -152,8 +168,67 @@ cd coderai
./build.sh vulkan # AMD/Intel only
```
Platform-specific alternatives:
```bash
./osxbuild.sh all # macOS, prefers Metal-backed builds when available
```
```powershell
.\build.ps1 -Backend all # Windows, prefers CUDA-backed builds when available
```
Packaging options:
```bash
./build.sh all --package
./osxbuild.sh all --package
```
```powershell
.\build.ps1 -Backend all -Package
```
`--package` installs PyInstaller into the build virtual environment and produces a self-contained distributable from the venv that was just created or updated.
Packaging outputs:
- Linux: `dist-package/coderai`
- macOS: `dist-package/coderai` and `dist-package/CoderAI.app`
- Windows: `dist-package/coderai.exe`
Packaging notes:
- macOS does have an equivalent to a standalone packaged app: a `.app` bundle. `osxbuild.sh --package` now builds both a single CLI binary and a macOS app bundle.
- These packages bundle the Python interpreter and Python modules from the venv, but they do not eliminate the need for compatible external GPU/runtime drivers on the target machine.
- CUDA builds on Linux and Windows still require matching NVIDIA driver/runtime support on the destination system.
- Metal builds on macOS still require a compatible macOS system with Metal support.
The build script creates a virtual environment, installs dependencies, and builds GPU-accelerated backends including `stable-diffusion-cpp-python` with CUDA+Vulkan support.
Platform backend notes:
- Linux: CUDA for NVIDIA, Vulkan for AMD/Intel/NVIDIA, OpenCL fallback where supported.
- macOS: Metal is the correct GPU acceleration path instead of CUDA. `osxbuild.sh` uses PyTorch MPS plus `GGML_METAL` / `SD_METAL` builds where available.
- Windows: CUDA remains the primary NVIDIA acceleration path. `build.ps1` focuses on CUDA or CPU installs.
- There is no general-purpose CUDA workflow for current macOS systems; Apple GPU acceleration uses Metal.
### Platform Support Matrix
| Capability | Linux | macOS | Windows |
|---|---|---|---|
| Core server / admin UI | Yes | Yes | Yes |
| Default path handling | Yes | Yes | Yes |
| PyTorch GPU acceleration | CUDA | Metal (MPS) | CUDA |
| `llama-cpp-python` GPU path | CUDA / Vulkan | Metal | CUDA |
| `stable-diffusion-cpp-python` GPU path | CUDA / Vulkan / OpenCL | Metal | CUDA |
| `whisper.cpp` accelerated path | Vulkan / CPU fallback | Metal / CPU fallback | CPU fallback |
| InsightFace / ONNX runtime | `onnxruntime-gpu` | `onnxruntime-silicon` or CPU | `onnxruntime-gpu` |
| Build script included in repo | `build.sh` | `osxbuild.sh` | `build.ps1` |
Notes:
- "Yes" means CoderAI has an intended path for that platform, not that every optional dependency is guaranteed to install on every machine.
- macOS GPU acceleration is Metal-based; there is no standard modern CUDA path for macOS.
- Windows currently uses CUDA as the main NVIDIA acceleration path; Vulkan/OpenCL build flows are not the primary Windows setup in this repository.
- Some optional audio and media packages may still vary by Python version, hardware, and upstream wheel availability.
### Manual Installation
```bash
......@@ -247,6 +322,45 @@ Config files live in `~/.coderai/` (or `--config` path):
└── secret_key # Session signing key (auto-generated)
```
### AISBF Broker Client
CoderAI includes an AISBF broker websocket client that can register this instance
with a broker and receive brokered requests.
You can configure it either by editing `config.json` directly or from the admin
Settings page under `AISBF Broker`.
Example broker configuration:
```json
{
"broker": {
"enabled": true,
"base_url": "https://broker.example.com",
"scope": "user",
"username": "alice",
"provider_id": "coderai-local",
"client_id": "workstation-01",
"registration_token": "your-registration-token",
"advertised_endpoint": "http://127.0.0.1:8776",
"transport": "websocket",
"heartbeat_interval_seconds": 30,
"connect_timeout_seconds": 10,
"request_timeout_seconds": 30,
"reconnect_initial_delay_seconds": 1,
"reconnect_max_delay_seconds": 60
}
}
```
Broker notes:
- `base_url` accepts `http`, `https`, `ws`, or `wss`; the websocket route is derived automatically.
- `scope: "user"` requires a non-global `username`.
- `scope: "global"` requires `username: "global"`.
- When `enabled` is `true`, `provider_id`, `client_id`, and `registration_token` are required.
- `advertised_endpoint` is optional and is sent to the broker as the externally reachable endpoint for this instance.
- Restart CoderAI after changing broker settings so the background broker service reconnects with the new configuration.
### config.json
```json
......
param(
[string]$Backend = "all",
[string]$Venv = "",
[switch]$Package
)
$ErrorActionPreference = "Stop"
$Backend = $Backend.ToLowerInvariant()
$validBackends = @("cuda", "cpu", "all")
if ($validBackends -notcontains $Backend) {
throw "Invalid backend '$Backend'. Use one of: cuda, cpu, all"
}
function Write-Info($msg) { Write-Host $msg -ForegroundColor Cyan }
function Write-Warn($msg) { Write-Host $msg -ForegroundColor Yellow }
function Write-Ok($msg) { Write-Host $msg -ForegroundColor Green }
Write-Info "========================================"
Write-Info " CoderAI Windows Build Script"
Write-Info " Backend: $Backend"
Write-Info "========================================"
$python = if (Get-Command py -ErrorAction SilentlyContinue) { "py" } else { "python" }
$pythonArgs = if ($python -eq "py") { @("-3") } else { @() }
if ([string]::IsNullOrWhiteSpace($Venv)) {
switch ($Backend) {
"cuda" { $Venv = "venv_win_cuda" }
"cpu" { $Venv = "venv_win_cpu" }
default { $Venv = "venv_win_all" }
}
}
if (-not (Test-Path $Venv)) {
Write-Info "Creating virtual environment: $Venv"
& $python @pythonArgs -m venv $Venv
} else {
Write-Warn "Using existing virtual environment: $Venv"
}
$venvPython = Join-Path $Venv "Scripts\python.exe"
$venvActivate = Join-Path $Venv "Scripts\Activate.ps1"
& $venvPython -m pip install --upgrade pip setuptools wheel
& $venvPython -m pip install -r requirements.txt
function Install-CommonMLStack {
& $venvPython -m pip install "imageio[ffmpeg]" scipy soundfile sentence-transformers openai-whisper argostranslate edge-tts kokoro-tts timm
& $venvPython -m pip install realesrgan basicsr
& $venvPython -m pip install demucs deepfilternet rnnoise voicefixer
& $venvPython -m pip install insightface onnxruntime-gpu
& $venvPython -m pip install f5-tts seed-vc
try { & $venvPython -m pip install audiocraft } catch { Write-Warn "audiocraft install failed; continuing" }
}
function Install-CudaStack {
Write-Info "Installing PyTorch with CUDA support..."
& $venvPython -m pip install torch torchvision torchaudio
Write-Info "Installing NVIDIA-specific requirements..."
try { & $venvPython -m pip install -r requirements-nvidia.txt } catch { Write-Warn "Some NVIDIA-specific packages failed; continuing" }
Write-Info "Installing llama-cpp-python with CUDA support..."
$env:CMAKE_ARGS = "-DGGML_CUDA=ON"
try { & $venvPython -m pip install --upgrade llama-cpp-python --no-cache-dir } finally { Remove-Item Env:CMAKE_ARGS -ErrorAction SilentlyContinue }
Write-Info "Installing stable-diffusion-cpp-python with CUDA support..."
$env:CMAKE_ARGS = "-DSD_CUDA=ON -DSD_WEBM=OFF"
try { & $venvPython -m pip install stable-diffusion-cpp-python --no-cache-dir } catch { Write-Warn "stable-diffusion-cpp-python CUDA build failed; continuing" } finally { Remove-Item Env:CMAKE_ARGS -ErrorAction SilentlyContinue }
}
function Install-CpuStack {
Write-Info "Installing CPU-oriented runtime..."
& $venvPython -m pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
& $venvPython -m pip install --upgrade llama-cpp-python
try { & $venvPython -m pip install stable-diffusion-cpp-python } catch { Write-Warn "stable-diffusion-cpp-python CPU install failed; continuing" }
try { & $venvPython -m pip install onnxruntime } catch { Write-Warn "onnxruntime CPU install failed; continuing" }
}
function Invoke-PackageBuild {
Write-Info "Packaging CoderAI with PyInstaller..."
& $venvPython -m pip install pyinstaller
New-Item -ItemType Directory -Force -Path "dist-package" | Out-Null
& $venvPython -m PyInstaller --clean --noconfirm --onefile --name coderai `
--collect-all codai `
--collect-all fastapi `
--collect-all uvicorn `
--collect-all pydantic `
--collect-all transformers `
--collect-all diffusers `
--collect-all sentence_transformers `
--collect-all whispercpp `
--collect-all insightface `
--collect-all onnxruntime `
--collect-all PIL `
coderai
Copy-Item "dist\coderai.exe" "dist-package\coderai.exe" -Force
Write-Ok "Packaged executable: dist-package\coderai.exe"
Write-Warn "Target systems still need compatible GPU/runtime drivers such as CUDA when GPU backends are used."
}
switch ($Backend) {
"cuda" {
Install-CudaStack
Install-CommonMLStack
}
"cpu" {
Install-CpuStack
Install-CommonMLStack
}
default {
Install-CudaStack
Install-CommonMLStack
}
}
Set-Content -Path ".backend" -Value $Backend
if ($Package) {
Invoke-PackageBuild
}
Write-Ok "Build completed successfully!"
Write-Host ""
Write-Host "To activate the environment in the future, run:"
Write-Host " $venvActivate"
Write-Host ""
Write-Host "Recommended runtime notes:"
Write-Host " - Windows keeps CUDA as the primary NVIDIA acceleration path"
Write-Host " - Metal is macOS-only and not used on Windows"
......@@ -16,7 +16,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# Build script for CoderAI - Supports NVIDIA (CUDA), Vulkan, OpenCL, and CPU backends
# Usage: ./build.sh [nvidia|vulkan|vulkan-nvidia|cuda|opencl|all] [--flash] [--venv <venv>]
# Usage: ./build.sh [nvidia|vulkan|vulkan-nvidia|cuda|opencl|all] [--flash] [--venv <venv>] [--package]
# Default: all (installs all backends)
# --flash: Enable and install Flash Attention 2 (for NVIDIA GPUs)
# --venv <venv>: Specify custom virtual environment name
......@@ -34,6 +34,7 @@ NC='\033[0m' # No Color
BACKEND="${1:-all}"
FLASH=false
CUSTOM_VENV=""
PACKAGE=false
# Parse arguments
i=1
......@@ -46,6 +47,9 @@ for arg in "$@"; do
i=$((i + 1))
eval "CUSTOM_VENV=\${$i}"
;;
--package)
PACKAGE=true
;;
esac
i=$((i + 1))
done
......@@ -719,10 +723,36 @@ elif [ "$BACKEND" = "all" ]; then
echo ""
fi
package_app() {
echo -e "${YELLOW}Packaging CoderAI with PyInstaller...${NC}"
pip install pyinstaller
mkdir -p dist-package
pyinstaller --clean --noconfirm --onefile --name coderai \
--collect-all codai \
--collect-all fastapi \
--collect-all uvicorn \
--collect-all pydantic \
--collect-all transformers \
--collect-all diffusers \
--collect-all sentence_transformers \
--collect-all whispercpp \
--collect-all insightface \
--collect-all onnxruntime \
--collect-all PIL \
coderai
cp dist/coderai dist-package/coderai
echo -e "${GREEN}✓ Packaged executable: dist-package/coderai${NC}"
echo -e "${YELLOW}Note: The target machine must still provide compatible system GPU/runtime libraries.${NC}"
}
# Create .backend file to track which backend was used
echo "$BACKEND" > .backend
if [ "$PACKAGE" = true ]; then
package_app
fi
echo -e "${GREEN}Build completed successfully!${NC}"
echo ""
echo "To activate the environment in the future, run:"
echo " source $VENV_DIR/bin/activate"
\ No newline at end of file
echo " source $VENV_DIR/bin/activate"
......@@ -54,7 +54,10 @@ def get_or_create_secret(config_dir: Path) -> bytes:
secret = secrets.token_bytes(32)
with open(secret_path, 'wb') as f:
f.write(secret)
secret_path.chmod(0o600)
try:
secret_path.chmod(0o600)
except OSError:
pass
return secret
......
......@@ -25,6 +25,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse, Stre
from fastapi.templating import Jinja2Templates
from codai.admin.auth import SessionManager
from codai.platform_paths import default_whisper_server_path
import queue as _q
import threading as _t
import uuid as _uuid
......@@ -90,7 +91,7 @@ def _next_whisper_server_model_id(audio_models) -> str:
def _default_whisper_server_path() -> str:
return shutil.which("whisper-server") or "/usr/local/bin/whisper-server"
return shutil.which("whisper-server") or default_whisper_server_path()
def get_current_user(request: Request) -> Optional[str]:
......@@ -239,7 +240,11 @@ async def admin_dashboard(request: Request, username: str = Depends(require_auth
@router.get("/admin/models", response_class=HTMLResponse)
async def models_page(request: Request, username: str = Depends(require_admin)):
return _tmpl(request, "models.html", {"username": username, "is_admin": True})
return _tmpl(request, "models.html", {
"username": username,
"is_admin": True,
"default_whisper_server_path": _default_whisper_server_path(),
})
@router.get("/admin/tokens", response_class=HTMLResponse)
......@@ -1434,12 +1439,14 @@ async def api_model_configure(request: Request, username: str = Depends(require_
gpu_device = int(data.get("gpu_device", 0))
if gpu_device < 0:
raise HTTPException(status_code=400, detail="gpu_device must be >= 0")
# Remove existing entry with same id (update semantics)
audio_list = config_manager.models_data.get("audio_models", [])
config_manager.models_data["audio_models"] = [
m for m in audio_list
if not (isinstance(m, dict) and m.get("id") == model_id)
]
if any(
isinstance(m, dict)
and m.get("backend") == "whisper-server"
and m.get("id") == model_id
for m in audio_list
):
raise HTTPException(status_code=409, detail=f"whisper-server model '{model_id}' already exists")
alias = (data.get("alias") or "").strip() or None
entry = {
"id": model_id,
......@@ -1617,6 +1624,22 @@ async def api_get_settings(username: str = Depends(require_admin)):
"directory": c.archive.directory,
"retention": c.archive.retention,
},
"broker": {
"enabled": c.broker.enabled,
"base_url": c.broker.base_url,
"scope": c.broker.scope,
"username": c.broker.username,
"provider_id": c.broker.provider_id,
"client_id": c.broker.client_id,
"registration_token": c.broker.registration_token,
"advertised_endpoint": c.broker.advertised_endpoint,
"transport": c.broker.transport,
"heartbeat_interval_seconds": c.broker.heartbeat_interval_seconds,
"connect_timeout_seconds": c.broker.connect_timeout_seconds,
"request_timeout_seconds": c.broker.request_timeout_seconds,
"reconnect_initial_delay_seconds": c.broker.reconnect_initial_delay_seconds,
"reconnect_max_delay_seconds": c.broker.reconnect_max_delay_seconds,
},
"system_prompt": c.system_prompt,
"tools_closer_prompt": c.tools_closer_prompt,
"grammar_guided": c.grammar_guided,
......@@ -1707,6 +1730,28 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad
resolved = raw_dir if raw_dir and _os.path.isabs(raw_dir) else _os.path.join(cfg_dir, raw_dir or "archive")
archive_manager.configure(c.archive.enabled, resolved, c.archive.retention)
if "broker" in data:
bro = data["broker"]
c.broker.enabled = bool(bro.get("enabled", c.broker.enabled))
c.broker.base_url = (bro.get("base_url") or "").strip()
c.broker.scope = (bro.get("scope") or c.broker.scope or "user").strip()
c.broker.username = (bro.get("username") or "").strip()
c.broker.provider_id = (bro.get("provider_id") or "").strip()
c.broker.client_id = (bro.get("client_id") or "").strip()
c.broker.registration_token = (bro.get("registration_token") or "").strip()
c.broker.advertised_endpoint = (bro.get("advertised_endpoint") or "").strip()
c.broker.transport = (bro.get("transport") or c.broker.transport or "websocket").strip()
c.broker.heartbeat_interval_seconds = max(1, int(bro.get("heartbeat_interval_seconds", c.broker.heartbeat_interval_seconds)))
c.broker.connect_timeout_seconds = max(1, int(bro.get("connect_timeout_seconds", c.broker.connect_timeout_seconds)))
c.broker.request_timeout_seconds = max(1, int(bro.get("request_timeout_seconds", c.broker.request_timeout_seconds)))
c.broker.reconnect_initial_delay_seconds = max(1, int(bro.get("reconnect_initial_delay_seconds", c.broker.reconnect_initial_delay_seconds)))
c.broker.reconnect_max_delay_seconds = max(
c.broker.reconnect_initial_delay_seconds,
int(bro.get("reconnect_max_delay_seconds", c.broker.reconnect_max_delay_seconds)),
)
from codai.broker.config import build_broker_runtime_config
request.app.state.broker_runtime = build_broker_runtime_config(c.broker)
config_manager.save_config()
return {"success": True}
......
......@@ -26,6 +26,9 @@
{% endblock %}
{% block content %}
<script>
window.__DEFAULT_WHISPER_SERVER_PATH__ = {{ default_whisper_server_path|tojson }};
</script>
<div class="page-header">
<div>
<h1>Models</h1>
......@@ -1253,7 +1256,7 @@ function nextWhisperServerModelId(){
}
function defaultWhisperServerPath(){
return '/usr/local/bin/whisper-server';
return window.__DEFAULT_WHISPER_SERVER_PATH__ || '';
}
function resetWhisperServerBuilderDefaults(){
......
......@@ -101,6 +101,81 @@
<span class="form-hint">Archived entries older than this are automatically deleted. Takes effect immediately on save.</span>
</div>
</div>
<div class="card mb-0" style="margin-top:1rem">
<div class="card-title">AISBF Broker</div>
<div class="form-row">
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
<input type="checkbox" id="s-broker-enabled" onchange="toggleBrokerFields()">
<span style="font-size:13px;font-weight:500">Enable broker client</span>
</label>
<span class="form-hint">Registers this CoderAI instance with an AISBF broker so it can receive brokered requests.</span>
</div>
<div id="broker-fields" style="display:none">
<div class="form-row">
<label class="form-label">Broker base URL</label>
<input type="text" id="s-broker-base-url" class="form-input" placeholder="https://broker.example.com">
<span class="form-hint">Supports `http`, `https`, `ws`, or `wss`. The websocket endpoint is derived automatically.</span>
</div>
<div style="display:grid;grid-template-columns:180px 1fr;gap:1rem;align-items:start">
<div class="form-row" style="margin:0">
<label class="form-label">Scope</label>
<select id="s-broker-scope" class="form-input">
<option value="user">user</option>
<option value="global">global</option>
</select>
</div>
<div class="form-row" style="margin:0">
<label class="form-label">Username</label>
<input type="text" id="s-broker-username" class="form-input" placeholder="alice or global">
<span class="form-hint">Use `global` when scope is `global`; otherwise provide the AISBF username.</span>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;align-items:start">
<div class="form-row" style="margin:0">
<label class="form-label">Provider ID</label>
<input type="text" id="s-broker-provider-id" class="form-input" placeholder="coderai-local">
</div>
<div class="form-row" style="margin:0">
<label class="form-label">Client ID</label>
<input type="text" id="s-broker-client-id" class="form-input" placeholder="workstation-01">
</div>
</div>
<div class="form-row">
<label class="form-label">Registration token</label>
<input type="password" id="s-broker-registration-token" class="form-input" placeholder="Broker registration token">
</div>
<div class="form-row">
<label class="form-label">Advertised endpoint</label>
<input type="text" id="s-broker-advertised-endpoint" class="form-input" placeholder="http://127.0.0.1:8776">
<span class="form-hint">Optional external URL advertised to the broker for this instance.</span>
</div>
<div style="display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:1rem;align-items:start">
<div class="form-row" style="margin:0">
<label class="form-label">Heartbeat seconds</label>
<input type="number" id="s-broker-heartbeat" class="form-input" min="1" placeholder="30">
</div>
<div class="form-row" style="margin:0">
<label class="form-label">Connect timeout</label>
<input type="number" id="s-broker-connect-timeout" class="form-input" min="1" placeholder="10">
</div>
<div class="form-row" style="margin:0">
<label class="form-label">Request timeout</label>
<input type="number" id="s-broker-request-timeout" class="form-input" min="1" placeholder="30">
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:1rem;align-items:start">
<div class="form-row" style="margin:0">
<label class="form-label">Reconnect initial delay</label>
<input type="number" id="s-broker-reconnect-initial" class="form-input" min="1" placeholder="1">
</div>
<div class="form-row" style="margin:0">
<label class="form-label">Reconnect max delay</label>
<input type="number" id="s-broker-reconnect-max" class="form-input" min="1" placeholder="60">
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
......@@ -110,6 +185,11 @@ function toggleHttps(){
document.getElementById('s-https').checked ? 'block' : 'none';
}
function toggleBrokerFields(){
document.getElementById('broker-fields').style.display =
document.getElementById('s-broker-enabled').checked ? 'block' : 'none';
}
function showAlert(type, msg){
const el = document.getElementById('settings-alert');
el.className = 'alert alert-' + (type === 'error' ? 'error' : 'info');
......@@ -143,6 +223,21 @@ async function loadSettings(){
if (hint && as.default_directory) hint.textContent = `(default: ${as.default_directory})`;
document.getElementById('s-arc-dir').placeholder = as.default_directory || '(default)';
} catch(_){}
const broker = d.broker || {};
document.getElementById('s-broker-enabled').checked = !!broker.enabled;
document.getElementById('s-broker-base-url').value = broker.base_url ?? '';
document.getElementById('s-broker-scope').value = broker.scope ?? 'user';
document.getElementById('s-broker-username').value = broker.username ?? '';
document.getElementById('s-broker-provider-id').value = broker.provider_id ?? '';
document.getElementById('s-broker-client-id').value = broker.client_id ?? '';
document.getElementById('s-broker-registration-token').value = broker.registration_token ?? '';
document.getElementById('s-broker-advertised-endpoint').value = broker.advertised_endpoint ?? '';
document.getElementById('s-broker-heartbeat').value = broker.heartbeat_interval_seconds ?? 30;
document.getElementById('s-broker-connect-timeout').value = broker.connect_timeout_seconds ?? 10;
document.getElementById('s-broker-request-timeout').value = broker.request_timeout_seconds ?? 30;
document.getElementById('s-broker-reconnect-initial').value = broker.reconnect_initial_delay_seconds ?? 1;
document.getElementById('s-broker-reconnect-max').value = broker.reconnect_max_delay_seconds ?? 60;
toggleBrokerFields();
}catch(e){ showAlert('error','Failed to load settings: '+e.message); }
}
......@@ -169,6 +264,22 @@ async function saveSettings(){
directory: document.getElementById('s-arc-dir').value.trim(),
retention: document.getElementById('s-arc-retention').value,
},
broker:{
enabled: document.getElementById('s-broker-enabled').checked,
base_url: document.getElementById('s-broker-base-url').value.trim(),
scope: document.getElementById('s-broker-scope').value,
username: document.getElementById('s-broker-username').value.trim(),
provider_id: document.getElementById('s-broker-provider-id').value.trim(),
client_id: document.getElementById('s-broker-client-id').value.trim(),
registration_token: document.getElementById('s-broker-registration-token').value.trim(),
advertised_endpoint: document.getElementById('s-broker-advertised-endpoint').value.trim(),
heartbeat_interval_seconds: parseInt(document.getElementById('s-broker-heartbeat').value) || 30,
connect_timeout_seconds: parseInt(document.getElementById('s-broker-connect-timeout').value) || 10,
request_timeout_seconds: parseInt(document.getElementById('s-broker-request-timeout').value) || 30,
reconnect_initial_delay_seconds: parseInt(document.getElementById('s-broker-reconnect-initial').value) || 1,
reconnect_max_delay_seconds: parseInt(document.getElementById('s-broker-reconnect-max').value) || 60,
transport: 'websocket',
},
};
try{
const r = await fetch(ROOT_PATH + '/admin/api/settings',{
......
......@@ -40,6 +40,8 @@ from typing import List, Optional
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, ConfigDict
from codai.platform_paths import default_characters_dir, legacy_style_config_dir
router = APIRouter()
_CHARS_DIR: Optional[str] = None
......@@ -47,8 +49,8 @@ _CHARS_DIR: Optional[str] = None
def set_global_args(args):
global _CHARS_DIR
base = getattr(args, 'file_path', None) or os.path.expanduser('~/.coderai')
root = base if os.path.isdir(base) else (os.path.dirname(base) if base else os.path.expanduser('~/.coderai'))
base = getattr(args, 'file_path', None) or str(legacy_style_config_dir())
root = base if os.path.isdir(base) else (os.path.dirname(base) if base else str(legacy_style_config_dir()))
_CHARS_DIR = os.path.join(root, 'characters')
os.makedirs(_CHARS_DIR, exist_ok=True)
......@@ -60,7 +62,7 @@ def set_global_file_path(path: str):
def _chars_dir() -> str:
if _CHARS_DIR:
return _CHARS_DIR
d = os.path.expanduser('~/.coderai/characters')
d = str(default_characters_dir())
os.makedirs(d, exist_ok=True)
return d
......
......@@ -42,6 +42,8 @@ from typing import List, Optional
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, ConfigDict
from codai.platform_paths import default_environments_dir, legacy_style_config_dir
router = APIRouter()
_ENVS_DIR: Optional[str] = None
......@@ -49,8 +51,8 @@ _ENVS_DIR: Optional[str] = None
def set_global_args(args):
global _ENVS_DIR
base = getattr(args, 'file_path', None) or os.path.expanduser('~/.coderai')
root = base if os.path.isdir(base) else (os.path.dirname(base) if base else os.path.expanduser('~/.coderai'))
base = getattr(args, 'file_path', None) or str(legacy_style_config_dir())
root = base if os.path.isdir(base) else (os.path.dirname(base) if base else str(legacy_style_config_dir()))
_ENVS_DIR = os.path.join(root, 'environments')
os.makedirs(_ENVS_DIR, exist_ok=True)
......@@ -62,7 +64,7 @@ def set_global_file_path(path: str):
def _envs_dir() -> str:
if _ENVS_DIR:
return _ENVS_DIR
d = os.path.expanduser('~/.coderai/environments')
d = str(default_environments_dir())
os.makedirs(d, exist_ok=True)
return d
......
......@@ -20,13 +20,14 @@ from PIL import Image
from pydantic import BaseModel, ConfigDict
from codai.api.images import save_image_response
from codai.platform_paths import default_insightface_model_path
router = APIRouter()
global_args = None
global_file_path = None
_INSWAPPER_MODEL_PATH = os.path.expanduser('~/.insightface/models/inswapper_128.onnx')
_INSWAPPER_MODEL_PATH = str(default_insightface_model_path())
_INSWAPPER_HF_REPO = 'deepinsight/inswapper'
_INSWAPPER_HF_FILE = 'inswapper_128.onnx'
......
......@@ -1713,7 +1713,8 @@ def _run_unpixelate(image_bytes: bytes, scale: int, model_path: Optional[str]) -
mp = model_path
else:
# Download RealESRGAN_x4plus on demand
mp = os.path.expanduser('~/.cache/realesrgan/RealESRGAN_x4plus.pth')
from codai.platform_paths import default_realesrgan_model_path
mp = str(default_realesrgan_model_path())
if not os.path.exists(mp):
os.makedirs(os.path.dirname(mp), exist_ok=True)
import urllib.request
......
......@@ -136,9 +136,11 @@ async def create_transcription(
# Check if the requested model maps to a configured whisper-server instance first.
# Try alias round-robin resolution before direct ID lookup.
whisper_model_id = multi_model_manager.resolve_whisper_alias_model_id(model)
whisper_server = (
multi_model_manager.resolve_whisper_alias(model)
or multi_model_manager.whisper_servers.get(model)
multi_model_manager.whisper_servers.get(whisper_model_id)
if whisper_model_id is not None
else multi_model_manager.whisper_servers.get(model)
)
if whisper_server is not None:
multi_model_manager.request_model(requested_model=model, model_type="audio")
......@@ -148,7 +150,7 @@ async def create_transcription(
gpu_device=getattr(whisper_server, "_gpu_device", 0),
)
if whisper_server.is_running():
ws_key = f"audio:{model}"
ws_key = f"audio:{whisper_model_id or model}"
multi_model_manager.models[ws_key] = whisper_server
multi_model_manager.active_in_vram = ws_key
multi_model_manager.models_in_vram.add(ws_key)
......
......@@ -22,6 +22,8 @@ from typing import Optional
from fastapi import APIRouter, HTTPException, Request, UploadFile, File, Form
from pydantic import BaseModel, ConfigDict
from codai.platform_paths import default_voices_dir
router = APIRouter()
global_args = None
......@@ -35,8 +37,8 @@ def set_global_args(args):
global global_args, _VOICES_DIR
global_args = args
# Store voice profiles alongside output files, or in a default location
base = getattr(args, 'file_path', None) or os.path.expanduser('~/.coderai/voices')
_VOICES_DIR = os.path.join(base if os.path.isdir(base) else os.path.dirname(base) if base else os.path.expanduser('~/.coderai'), 'voices')
base = getattr(args, 'file_path', None) or str(default_voices_dir())
_VOICES_DIR = os.path.join(base if os.path.isdir(base) else os.path.dirname(base) if base else str(default_voices_dir().parent), 'voices')
os.makedirs(_VOICES_DIR, exist_ok=True)
......@@ -48,7 +50,7 @@ def set_global_file_path(path):
def _voices_dir() -> str:
if _VOICES_DIR:
return _VOICES_DIR
d = os.path.expanduser('~/.coderai/voices')
d = str(default_voices_dir())
os.makedirs(d, exist_ok=True)
return d
......
......@@ -20,6 +20,8 @@ import json
import os
from pathlib import Path
from codai.platform_paths import legacy_style_config_dir
def load_config_file(config_dir: Path) -> dict:
"""Load the main config.json file."""
......@@ -166,11 +168,12 @@ def setup_default_config(config_dir: Path):
def parse_args():
"""Parse command line arguments."""
default_config = str(legacy_style_config_dir())
parser = argparse.ArgumentParser(
description="OpenAI-compatible API server supporting NVIDIA (CUDA) and Vulkan backends",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""Configuration: All settings are loaded from JSON config files in the
configuration directory (--config DIR, default: ~/.coderai/). Key files:
configuration directory (--config DIR, default: OS-specific CoderAI directory). Key files:
config.json - Server and backend settings
models.json - Model registry and configurations
auth.json - Users, tokens, and sessions"""
......@@ -178,8 +181,8 @@ configuration directory (--config DIR, default: ~/.coderai/). Key files:
parser.add_argument(
"--config",
type=str,
default=os.path.expanduser("~/.coderai/"),
help="Configuration directory (default: ~/.coderai/)",
default=default_config,
help=f"Configuration directory (default: {default_config})",
)
parser.add_argument(
"--debug",
......@@ -224,4 +227,4 @@ configuration directory (--config DIR, default: ~/.coderai/). Key files:
action="store_true",
help="List available Vulkan GPU devices and exit",
)
return parser.parse_args()
\ No newline at end of file
return parser.parse_args()
......@@ -40,6 +40,8 @@ import hashlib
import pathlib
from typing import Optional, Dict, List, Tuple
from codai.platform_paths import default_model_cache_dir, legacy_style_cache_root
# For type hints
import time
......@@ -49,8 +51,7 @@ def get_model_cache_dir() -> str:
if os.environ.get('CODERAI_CACHE_DIR'):
cache_dir = os.environ['CODERAI_CACHE_DIR']
else:
cache_home = os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
cache_dir = os.path.join(cache_home, 'coderai', 'models')
cache_dir = str(default_model_cache_dir())
pathlib.Path(cache_dir).mkdir(parents=True, exist_ok=True)
return cache_dir
......@@ -58,7 +59,7 @@ def get_model_cache_dir() -> str:
def get_all_cache_dirs() -> dict:
"""Get all model cache directories."""
caches = {}
cache_home = os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
cache_home = str(legacy_style_cache_root())
# Coderai GGUF cache
coderai_cache = get_model_cache_dir()
......@@ -79,7 +80,7 @@ def get_all_cache_dirs() -> dict:
caches['huggingface'] = hf_cache
# Local diffusers cache
local_diffusers = os.path.expanduser('~/.cache/diffusers')
local_diffusers = os.path.join(cache_home, 'diffusers')
if os.path.exists(local_diffusers):
caches['diffusers'] = local_diffusers
......@@ -549,4 +550,4 @@ __all__ = [
'remove_cached_model',
'list_cached_models_info',
'remove_all_cached_models',
]
\ No newline at end of file
]
......@@ -18,6 +18,7 @@
from typing import Optional, Dict, Any, List
import os
import random
import subprocess
import signal
import requests
......@@ -256,6 +257,7 @@ class WhisperServerManager:
self.current_model = None
self.base_url = f"http://127.0.0.1:{port}"
self.lock = threading.Lock()
self._active_requests = 0
# Check if port is available
if not self._is_port_available(port):
......@@ -351,7 +353,8 @@ class WhisperServerManager:
"""Send transcription request to whisper-server."""
if not self.is_running():
return {"error": "whisper-server not running"}
with self.lock:
self._active_requests += 1
try:
files = {"file": ("audio.wav", audio_data, "audio/wav")}
data = {}
......@@ -373,6 +376,16 @@ class WhisperServerManager:
return {"error": f"Server error: {response.status_code}", "detail": response.text}
except Exception as e:
return {"error": str(e)}
finally:
with self.lock:
self._active_requests = max(0, self._active_requests - 1)
def active_requests(self) -> int:
with self.lock:
return self._active_requests
def is_idle(self) -> bool:
return self.active_requests() == 0
def _wait_for_server(self, timeout: int = 30) -> bool:
"""Wait for whisper-server to be ready."""
......@@ -810,6 +823,17 @@ class MultiModelManager:
idx = self._whisper_alias_counters.get(name, 0) % len(ids)
self._whisper_alias_counters[name] = idx + 1
return self.whisper_servers.get(ids[idx])
def resolve_whisper_alias_model_id(self, name: str) -> Optional[str]:
"""Return an idle whisper-server model id for an alias, else a random one."""
ids = self.whisper_aliases.get(name)
if not ids:
return None
idle_ids = [mid for mid in ids if (self.whisper_servers.get(mid) and self.whisper_servers[mid].is_idle())]
choices = idle_ids or list(ids)
if len(choices) == 1:
return choices[0]
return random.choice(choices)
def set_tts_model(self, model_name: str, config: Dict = None):
"""Set the text-to-speech model and download/cache it if needed."""
......@@ -900,6 +924,9 @@ class MultiModelManager:
for m in self.audio_models:
allowed.add(m)
allowed.add(f"audio:{m}")
for alias in self.whisper_aliases:
allowed.add(alias)
allowed.add(f"audio:{alias}")
# TTS model
if self.tts_model:
......@@ -963,10 +990,18 @@ class MultiModelManager:
"video_models", "audio_gen_models", "embedding_models",
"spatial_models"):
for m in md.get(cat, []):
mid = (m if isinstance(m, str) else
m.get("alias") or m.get("path") or m.get("id") or "")
if isinstance(m, str):
mid = m
elif m.get("backend") == "whisper-server":
mid = m.get("id") or ""
else:
mid = m.get("alias") or m.get("path") or m.get("id") or ""
raw = (m if isinstance(m, str) else m.get("path") or m.get("id") or "")
for val in (mid, raw):
alias = "" if isinstance(m, str) else (m.get("alias") or "")
vals = [mid, raw]
if alias:
vals.append(alias)
for val in vals:
if val:
allowed.add(val)
short = val.split("/")[-1] if "/" in val else val
......
"""Platform-aware filesystem helpers for CoderAI."""
from __future__ import annotations
import os
from pathlib import Path
APP_NAME = "coderai"
def _home_dir() -> Path:
return Path.home()
def _windows_dir(env_var: str, fallback: Path) -> Path:
value = os.environ.get(env_var)
return Path(value).expanduser() if value else fallback
def user_config_dir() -> Path:
if os.name == "nt":
base = _windows_dir("APPDATA", _home_dir() / "AppData" / "Roaming")
return base / "CoderAI"
if sys_platform() == "darwin":
return _home_dir() / "Library" / "Application Support" / "CoderAI"
xdg = os.environ.get("XDG_CONFIG_HOME")
base = Path(xdg).expanduser() if xdg else (_home_dir() / ".config")
return base / APP_NAME
def user_data_dir() -> Path:
if os.name == "nt":
base = _windows_dir("LOCALAPPDATA", _home_dir() / "AppData" / "Local")
return base / "CoderAI"
if sys_platform() == "darwin":
return _home_dir() / "Library" / "Application Support" / "CoderAI"
xdg = os.environ.get("XDG_DATA_HOME")
base = Path(xdg).expanduser() if xdg else (_home_dir() / ".local" / "share")
return base / APP_NAME
def user_cache_dir() -> Path:
if os.name == "nt":
base = _windows_dir("LOCALAPPDATA", _home_dir() / "AppData" / "Local")
return base / "CoderAI" / "Cache"
if sys_platform() == "darwin":
return _home_dir() / "Library" / "Caches" / "CoderAI"
xdg = os.environ.get("XDG_CACHE_HOME")
base = Path(xdg).expanduser() if xdg else (_home_dir() / ".cache")
return base / APP_NAME
def ensure_dir(path: Path) -> Path:
path.mkdir(parents=True, exist_ok=True)
return path
def default_config_dir() -> Path:
return ensure_dir(user_config_dir())
def default_data_dir() -> Path:
return ensure_dir(user_data_dir())
def default_cache_dir() -> Path:
return ensure_dir(user_cache_dir())
def legacy_style_config_dir() -> Path:
if os.name == "nt":
return _home_dir() / ".coderai"
return _home_dir() / ".coderai"
def legacy_style_cache_root() -> Path:
if os.name == "nt":
base = _windows_dir("LOCALAPPDATA", _home_dir() / "AppData" / "Local")
return base / ".cache"
if sys_platform() == "darwin":
return _home_dir() / "Library" / "Caches"
xdg = os.environ.get("XDG_CACHE_HOME")
return Path(xdg).expanduser() if xdg else (_home_dir() / ".cache")
def default_model_cache_dir() -> Path:
return ensure_dir(legacy_style_cache_root() / APP_NAME / "models")
def default_diffusers_cache_dir() -> Path:
return ensure_dir(legacy_style_cache_root() / "diffusers")
def default_realesrgan_model_path() -> Path:
return default_diffusers_cache_dir().parent / "realesrgan" / "RealESRGAN_x4plus.pth"
def default_insightface_model_path() -> Path:
return ensure_dir(legacy_style_cache_root() / "insightface" / "models") / "inswapper_128.onnx"
def default_voices_dir() -> Path:
return ensure_dir(legacy_style_config_dir() / "voices")
def default_characters_dir() -> Path:
return ensure_dir(legacy_style_config_dir() / "characters")
def default_environments_dir() -> Path:
return ensure_dir(legacy_style_config_dir() / "environments")
def default_whisper_server_path() -> str:
if os.name == "nt":
local = _windows_dir("LOCALAPPDATA", _home_dir() / "AppData" / "Local")
return str(local / "Programs" / "whisper-server" / "whisper-server.exe")
if sys_platform() == "darwin":
return "/usr/local/bin/whisper-server"
return "/usr/local/bin/whisper-server"
def sys_platform() -> str:
return os.sys.platform
#!/bin/bash
# CoderAI macOS build script
# Usage: ./osxbuild.sh [metal|cpu|all] [--venv <venv>] [--package]
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
BACKEND="${1:-all}"
CUSTOM_VENV=""
PACKAGE=false
i=1
for arg in "$@"; do
case $arg in
--venv)
i=$((i + 1))
eval "CUSTOM_VENV=\${$i}"
;;
--package)
PACKAGE=true
;;
esac
i=$((i + 1))
done
BACKEND=$(echo "$BACKEND" | tr '[:upper:]' '[:lower:]')
if [[ "$BACKEND" != "metal" && "$BACKEND" != "cpu" && "$BACKEND" != "all" ]]; then
echo -e "${RED}Error: Invalid backend '$BACKEND'${NC}"
echo "Usage: ./osxbuild.sh [metal|cpu|all] [--venv <venv>]"
exit 1
fi
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} CoderAI macOS Build Script${NC}"
echo -e "${BLUE} Backend: ${GREEN}$BACKEND${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
PYTHON_BIN="${PYTHON_BIN:-python3}"
PYTHON_VERSION=$($PYTHON_BIN --version 2>&1 | python3 -c 'import sys,re; print(re.search(r"(\d+\.\d+)", sys.stdin.read()).group(1))')
echo -e "${GREEN}✓ Python version: $PYTHON_VERSION${NC}"
if [ -n "$CUSTOM_VENV" ]; then
VENV_DIR="$CUSTOM_VENV"
elif [ "$BACKEND" = "metal" ]; then
VENV_DIR="venv_osx_metal"
elif [ "$BACKEND" = "cpu" ]; then
VENV_DIR="venv_osx_cpu"
else
VENV_DIR="venv_osx_all"
fi
if [ ! -d "$VENV_DIR" ]; then
echo -e "${YELLOW}Creating virtual environment: $VENV_DIR${NC}"
"$PYTHON_BIN" -m venv "$VENV_DIR"
else
echo -e "${YELLOW}Using existing virtual environment: $VENV_DIR${NC}"
fi
source "$VENV_DIR/bin/activate"
export PIP_NO_INPUT=1
export PIP_REQUIRE_VIRTUALENV=1
python -m pip install --upgrade pip setuptools wheel
python -m pip install -r requirements.txt
install_common_ml_stack() {
python -m pip install "imageio[ffmpeg]" scipy soundfile sentence-transformers openai-whisper argostranslate edge-tts kokoro-tts timm || true
python -m pip install realesrgan basicsr || true
python -m pip install demucs deepfilternet rnnoise voicefixer || true
python -m pip install insightface onnxruntime-silicon || python -m pip install insightface onnxruntime || true
python -m pip install f5-tts seed-vc || true
python -m pip install audiocraft || true
}
install_metal_stack() {
echo -e "${YELLOW}Installing PyTorch with Apple Silicon / MPS support...${NC}"
python -m pip install torch torchvision torchaudio
echo -e "${YELLOW}Installing llama-cpp-python with Metal support...${NC}"
CMAKE_ARGS="-DGGML_METAL=ON" python -m pip install --upgrade llama-cpp-python --no-cache-dir || {
echo -e "${YELLOW}Warning: Metal build failed, installing CPU llama-cpp-python${NC}"
python -m pip install --upgrade llama-cpp-python
}
echo -e "${YELLOW}Installing stable-diffusion-cpp-python with Metal support...${NC}"
CMAKE_ARGS="-DSD_METAL=ON -DSD_WEBM=OFF" python -m pip install stable-diffusion-cpp-python --no-cache-dir || {
echo -e "${YELLOW}Warning: Metal stable-diffusion-cpp-python not available${NC}"
}
echo -e "${YELLOW}Installing whispercpp with Metal support when possible...${NC}"
python -m pip uninstall -y whispercpp >/dev/null 2>&1 || true
TMP_DIR="${TMPDIR:-/tmp}/coderai-whispercpp"
rm -rf "$TMP_DIR"
git clone --depth 1 https://github.com/ggerganov/whisper.cpp "$TMP_DIR" >/dev/null 2>&1 || true
if [ -d "$TMP_DIR/bindings/python" ]; then
(cd "$TMP_DIR/bindings/python" && CMAKE_ARGS="-DWHISPER_METAL=ON -DGGML_METAL=ON" python -m pip install . --no-cache-dir --force-reinstall) || \
(cd "$TMP_DIR/bindings/python" && python -m pip install . --no-cache-dir --force-reinstall) || true
fi
}
install_cpu_stack() {
echo -e "${YELLOW}Installing CPU-only runtime...${NC}"
python -m pip install torch torchvision torchaudio
python -m pip install --upgrade llama-cpp-python
python -m pip install stable-diffusion-cpp-python || true
}
package_app() {
echo -e "${YELLOW}Packaging CoderAI with PyInstaller...${NC}"
python -m pip install pyinstaller
mkdir -p dist-package
pyinstaller --clean --noconfirm --onefile --name coderai \
--collect-all codai \
--collect-all fastapi \
--collect-all uvicorn \
--collect-all pydantic \
--collect-all transformers \
--collect-all diffusers \
--collect-all sentence_transformers \
--collect-all whispercpp \
--collect-all insightface \
--collect-all onnxruntime \
--collect-all PIL \
coderai
pyinstaller --clean --noconfirm --windowed --name CoderAI \
--collect-all codai \
--collect-all fastapi \
--collect-all uvicorn \
--collect-all pydantic \
--collect-all transformers \
--collect-all diffusers \
--collect-all sentence_transformers \
--collect-all whispercpp \
--collect-all insightface \
--collect-all onnxruntime \
--collect-all PIL \
coderai
cp dist/coderai dist-package/coderai
if [ -d "dist/CoderAI.app" ]; then
rm -rf dist-package/CoderAI.app
cp -R dist/CoderAI.app dist-package/CoderAI.app
fi
echo -e "${GREEN}✓ Packaged CLI binary: dist-package/coderai${NC}"
echo -e "${GREEN}✓ Packaged macOS app bundle: dist-package/CoderAI.app${NC}"
echo -e "${YELLOW}Note: macOS equivalent packaging is a single CLI binary plus a .app bundle; target machines still need compatible GPU/runtime libraries.${NC}"
}
if [ "$BACKEND" = "metal" ]; then
install_metal_stack
install_common_ml_stack
elif [ "$BACKEND" = "cpu" ]; then
install_cpu_stack
install_common_ml_stack
else
install_metal_stack
install_common_ml_stack
fi
echo "$BACKEND" > .backend
if [ "$PACKAGE" = true ]; then
package_app
fi
echo -e "${GREEN}Build completed successfully!${NC}"
echo ""
echo "To activate the environment in the future, run:"
echo " source $VENV_DIR/bin/activate"
echo ""
echo "Recommended runtime notes:"
echo " - macOS uses Metal (MPS / GGML_METAL / SD_METAL) instead of CUDA"
echo " - NVIDIA CUDA is not the standard macOS path"
......@@ -247,6 +247,7 @@ def test_model_configure_allows_whisper_server_id_coexistence_with_other_audio_b
def test_model_configure_defaults_missing_whisper_server_path_to_usr_local_bin(monkeypatch, tmp_path):
from codai.admin import routes
from codai.platform_paths import default_whisper_server_path
cfg = _build_config(tmp_path)
monkeypatch.setattr(routes, "config_manager", cfg, raising=False)
......@@ -267,7 +268,7 @@ def test_model_configure_defaults_missing_whisper_server_path_to_usr_local_bin(m
)
assert response.status_code == 200
assert cfg.models_data["audio_models"][0]["server_path"] == "/usr/local/bin/whisper-server"
assert cfg.models_data["audio_models"][0]["server_path"] == default_whisper_server_path()
app.dependency_overrides.clear()
......@@ -546,6 +547,67 @@ def test_transcription_requires_configured_whisper_server_model_id():
assert "not configured" in str(exc.value.detail).lower() or "not available" in str(exc.value.detail).lower()
def test_whisper_alias_prefers_idle_instance(monkeypatch):
from codai.models.manager import MultiModelManager
manager = MultiModelManager()
busy = SimpleNamespace(is_idle=lambda: False)
idle = SimpleNamespace(is_idle=lambda: True)
manager.whisper_servers = {"whisper0": busy, "whisper1": idle}
manager.whisper_aliases = {"shared-whisper": ["whisper0", "whisper1"]}
assert manager.resolve_whisper_alias_model_id("shared-whisper") == "whisper1"
def test_whisper_alias_uses_random_choice_when_multiple_idle(monkeypatch):
from codai.models.manager import MultiModelManager
manager = MultiModelManager()
idle0 = SimpleNamespace(is_idle=lambda: True)
idle1 = SimpleNamespace(is_idle=lambda: True)
manager.whisper_servers = {"whisper0": idle0, "whisper1": idle1}
manager.whisper_aliases = {"shared-whisper": ["whisper0", "whisper1"]}
monkeypatch.setattr("codai.models.manager.random.choice", lambda items: items[-1])
assert manager.resolve_whisper_alias_model_id("shared-whisper") == "whisper1"
def test_get_all_allowed_identifiers_includes_whisper_alias(monkeypatch):
from codai.admin import routes
from codai.models.manager import MultiModelManager
manager = MultiModelManager()
manager.audio_models[:] = ["whisper0", "whisper1"]
manager.whisper_aliases = {"shared-whisper": ["whisper0", "whisper1"]}
monkeypatch.setattr(
routes,
"config_manager",
SimpleNamespace(
models_data={
"text_models": [],
"image_models": [],
"audio_models": [
{"id": "whisper0", "backend": "whisper-server", "alias": "shared-whisper"},
{"id": "whisper1", "backend": "whisper-server", "alias": "shared-whisper"},
],
"vision_models": [],
"tts_models": [],
"gguf_models": [],
"video_models": [],
"audio_gen_models": [],
"embedding_models": [],
"aliases": {},
}
),
raising=False,
)
allowed = manager.get_all_allowed_identifiers()
assert "shared-whisper" in allowed
assert "audio:shared-whisper" in allowed
def test_get_all_allowed_identifiers_includes_configured_whisper_server_id_without_legacy_alias(monkeypatch):
from codai.admin import routes
from codai.models.manager import MultiModelManager
......@@ -673,7 +735,7 @@ def test_models_template_defines_whisper_server_builder_default_helpers():
template = Path("codai/admin/templates/models.html").read_text()
assert "function defaultWhisperServerPath()" in template
assert "return '/usr/local/bin/whisper-server';" in template
assert "return window.__DEFAULT_WHISPER_SERVER_PATH__ || '';" in template
assert "function resetWhisperServerBuilderDefaults()" in template
assert "const modelIdInput = document.getElementById('ws-model-id');" in template
assert "const serverPathInput = document.getElementById('ws-server-path');" in template
......@@ -779,3 +841,15 @@ def test_settings_template_keeps_queue_size_control():
assert "Request queue max size" in template
assert "s-queue-max" in template
def test_settings_template_includes_broker_controls():
template = Path("codai/admin/templates/settings.html").read_text()
assert "AISBF Broker" in template
assert "s-broker-enabled" in template
assert "s-broker-base-url" in template
assert "s-broker-provider-id" in template
assert "s-broker-client-id" in template
assert "s-broker-registration-token" in template
assert "toggleBrokerFields()" in template
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment