feat(ollama): add availability check before model operations
Add `ensureOllamaAvailable` function to lib/ollama.ts for checking and waiting for Ollama server health with configurable timeout. Integrate the check in GET /api/ollama/models route with 3s timeout to prevent UI blocking, and in POST /api/ollama/pull route with 20s timeout to ensure availability before pulling models. Update ChatContainer to always render ModelDropdown for better UX when no model is selected.
This commit is contained in:
1 parent
f96d4ed696
commit
7b2386876a
12 files changed
+211
-20
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { listModelsViaSdk, listModelsViaHttp } from "@/lib/ollama";
|
import { listModelsViaSdk, listModelsViaHttp, ensureOllamaAvailable } from "@/lib/ollama";
|
||||||
import {
|
import {
|
||||||
estimateCpuVram,
|
estimateCpuVram,
|
||||||
inferDevice,
|
inferDevice,
|
||||||
@@ -31,6 +31,8 @@ function normalizeModel(model: any): ModelInfo {
|
|||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
let models: any[] = [];
|
let models: any[] = [];
|
||||||
|
// Tentar garantir disponibilidade rapidamente (não bloquear longamente a UI)
|
||||||
|
await ensureOllamaAvailable({ timeoutMs: 3000 }).catch(() => {});
|
||||||
try {
|
try {
|
||||||
models = await listModelsViaSdk();
|
models = await listModelsViaSdk();
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest } from "next/server";
|
import { NextRequest } from "next/server";
|
||||||
import { pullModel } from "@/lib/ollama";
|
import { pullModel, ensureOllamaAvailable } from "@/lib/ollama";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -14,6 +14,18 @@ export async function POST(req: NextRequest) {
|
|||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
try {
|
try {
|
||||||
|
// Garantir que o servidor esteja disponível antes do pull
|
||||||
|
const ok = await ensureOllamaAvailable({ timeoutMs: 20000 });
|
||||||
|
if (!ok) {
|
||||||
|
controller.enqueue(
|
||||||
|
new TextEncoder().encode(
|
||||||
|
JSON.stringify({ error: "Ollama indisponível" }) + "\n",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
controller.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await pullModel(model);
|
const response = await pullModel(model);
|
||||||
|
|
||||||
for await (const chunk of response) {
|
for await (const chunk of response) {
|
||||||
|
|||||||
@@ -435,20 +435,16 @@ export function ChatContainer({
|
|||||||
<div className="border-b border-[var(--border)] p-4 flex items-center justify-between gap-3 flex-shrink-0">
|
<div className="border-b border-[var(--border)] p-4 flex items-center justify-between gap-3 flex-shrink-0">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h1 className="text-lg font-semibold">
|
<h1 className="text-lg font-semibold">
|
||||||
{selectedModelData
|
{selectedModelData ? "Chat with" : "Select a model to start chatting"}
|
||||||
? "Chat with"
|
|
||||||
: "Select a model to start chatting"}
|
|
||||||
</h1>
|
</h1>
|
||||||
{selectedModelData && (
|
<ModelDropdown
|
||||||
<ModelDropdown
|
models={models}
|
||||||
models={models}
|
selectedModel={selectedModel}
|
||||||
selectedModel={selectedModel}
|
onSelectModel={handleSelectModel}
|
||||||
onSelectModel={handleSelectModel}
|
onDeleteModel={handleDeleteModel}
|
||||||
onDeleteModel={handleDeleteModel}
|
onPullModel={handlePullModel}
|
||||||
onPullModel={handlePullModel}
|
disabled={isStreaming}
|
||||||
disabled={isStreaming}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SettingsModal
|
<SettingsModal
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,6 @@
|
|||||||
import { Ollama } from "ollama";
|
import { Ollama } from "ollama";
|
||||||
|
import os from "node:os";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
|
||||||
export const OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434";
|
export const OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434";
|
||||||
|
|
||||||
@@ -20,6 +22,51 @@ export async function listModelsViaHttp() {
|
|||||||
return data?.models ?? [];
|
return data?.models ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function isOllamaHealthy(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await listModelsViaHttp();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitUntilHealthy(timeoutMs: number, intervalMs = 800): Promise<boolean> {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
if (await isOllamaHealthy()) return true;
|
||||||
|
await new Promise((r) => setTimeout(r, intervalMs));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureOllamaAvailable(options?: { timeoutMs?: number }): Promise<boolean> {
|
||||||
|
const timeoutMs = options?.timeoutMs ?? 20000;
|
||||||
|
if (await isOllamaHealthy()) return true;
|
||||||
|
|
||||||
|
// Windows: tentar iniciar via PowerShell script (WSL-first fallback Windows)
|
||||||
|
if (os.platform() === "win32") {
|
||||||
|
try {
|
||||||
|
const child = spawn(
|
||||||
|
"powershell",
|
||||||
|
[
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-File",
|
||||||
|
"scripts/ensure-ollama.ps1",
|
||||||
|
],
|
||||||
|
{ stdio: "ignore", detached: true }
|
||||||
|
);
|
||||||
|
child.unref();
|
||||||
|
} catch (e) {
|
||||||
|
// Ignorar, tentaremos apenas aguardar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aguardar saúde
|
||||||
|
return await waitUntilHealthy(timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
export async function chatWithStream(
|
export async function chatWithStream(
|
||||||
model: string,
|
model: string,
|
||||||
messages: Array<{ role: string; content: string }>,
|
messages: Array<{ role: string; content: string }>,
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predev": "tsx scripts/init-db.ts",
|
"predev": "powershell -ExecutionPolicy Bypass -File scripts/ensure-ollama.ps1 && tsx scripts/init-db.ts",
|
||||||
"prebuild": "tsx scripts/init-db.ts",
|
"prebuild": "tsx scripts/init-db.ts",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
ignoredBuiltDependencies:
|
ignoredBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
- sharp
|
- sharp
|
||||||
|
|
||||||
onlyBuiltDependencies:
|
onlyBuiltDependencies:
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Ensures Ollama is running on localhost using WSL (Ubuntu) when available,
|
||||||
|
# with a Windows-native fallback. Designed to be idempotent.
|
||||||
|
|
||||||
|
param()
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'SilentlyContinue'
|
||||||
|
|
||||||
|
function Write-Info($msg) { Write-Host "[ensure-ollama] $msg" }
|
||||||
|
function Write-Warn($msg) { Write-Warning "[ensure-ollama] $msg" }
|
||||||
|
|
||||||
|
function Get-EnvOrDefault([string]$name, [string]$default) {
|
||||||
|
$val = [Environment]::GetEnvironmentVariable($name)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($val)) { return $default }
|
||||||
|
return $val
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-Port([int]$Port) {
|
||||||
|
try {
|
||||||
|
$res = Test-NetConnection -ComputerName localhost -Port $Port -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
|
||||||
|
return [bool]$res.TcpTestSucceeded
|
||||||
|
} catch { return $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Wait-Healthy([int]$Port, [int]$TimeoutSeconds) {
|
||||||
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||||
|
while ((Get-Date) -lt $deadline) {
|
||||||
|
if (Test-Port -Port $Port) { return $true }
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Has-WSL() {
|
||||||
|
return $null -ne (Get-Command wsl.exe -ErrorAction SilentlyContinue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function WSL-HasDistro([string]$Distro) {
|
||||||
|
try {
|
||||||
|
$list = & wsl.exe -l -q 2>$null
|
||||||
|
return $list -split "`r?`n" | Where-Object { $_ -eq $Distro } | ForEach-Object { $true } | Select-Object -First 1
|
||||||
|
} catch { return $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-WSL([string]$Distro, [string]$Cmd) {
|
||||||
|
& wsl.exe -d $Distro -- bash -lc $Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Ollama-WSL([string]$Distro) {
|
||||||
|
Write-Info "Verificando/instalando Ollama na WSL ($Distro)"
|
||||||
|
$installCmd = 'command -v ollama >/dev/null 2>&1 || (curl -fsSL https://ollama.com/install.sh | sh)'
|
||||||
|
Invoke-WSL -Distro $Distro -Cmd $installCmd | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Start-Ollama-WSL([string]$Distro) {
|
||||||
|
Write-Info "Iniciando ollama serve em background (WSL:$Distro)"
|
||||||
|
$startCmd = 'mkdir -p "$HOME/.ollama"; nohup ollama serve > "$HOME/.ollama/ollama.log" 2>&1 & disown || true'
|
||||||
|
Invoke-WSL -Distro $Distro -Cmd $startCmd | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Has-Ollama-OnWindows() {
|
||||||
|
return $null -ne (Get-Command ollama -ErrorAction SilentlyContinue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Start-Ollama-Windows() {
|
||||||
|
try {
|
||||||
|
$ollama = (Get-Command ollama -ErrorAction Stop).Source
|
||||||
|
Write-Info "Iniciando ollama serve no Windows (hidden)"
|
||||||
|
Start-Process -FilePath $ollama -ArgumentList 'serve' -WindowStyle Hidden | Out-Null
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
Write-Warn "Ollama não encontrado no PATH do Windows."
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
$port = [int](Get-EnvOrDefault 'OLLAMA_PORT' '11434')
|
||||||
|
$timeout = [int](Get-EnvOrDefault 'OLLAMA_START_TIMEOUT' '20')
|
||||||
|
$distro = Get-EnvOrDefault 'OLLAMA_WSL_DISTRO' 'Ubuntu'
|
||||||
|
|
||||||
|
if (Test-Port -Port $port) {
|
||||||
|
Write-Info "Porta $port já está respondendo. Nada a fazer."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Has-WSL) {
|
||||||
|
if (-not (WSL-HasDistro -Distro $distro)) {
|
||||||
|
Write-Warn "Distro '$distro' não encontrada. Tentando WSL padrão..."
|
||||||
|
$distro = 'Ubuntu'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Tentando iniciar Ollama via WSL ($distro)"
|
||||||
|
Ensure-Ollama-WSL -Distro $distro
|
||||||
|
Start-Ollama-WSL -Distro $distro
|
||||||
|
|
||||||
|
if (Wait-Healthy -Port $port -TimeoutSeconds $timeout) {
|
||||||
|
Write-Info "Ollama disponível em localhost:$port (via WSL)."
|
||||||
|
exit 0
|
||||||
|
} else {
|
||||||
|
Write-Warn "Timeout aguardando Ollama (WSL). Prosseguindo com fallback Windows."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback Windows
|
||||||
|
if (-not (Has-Ollama-OnWindows)) {
|
||||||
|
Write-Warn "Ollama não encontrado no Windows e WSL não atendeu no tempo."
|
||||||
|
Write-Warn "Instale o Ollama no Windows ou habilite WSL/Ubuntu."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Start-Ollama-Windows) {
|
||||||
|
if (Wait-Healthy -Port $port -TimeoutSeconds $timeout) {
|
||||||
|
Write-Info "Ollama disponível em localhost:$port (Windows)."
|
||||||
|
exit 0
|
||||||
|
} else {
|
||||||
|
Write-Warn "Timeout aguardando Ollama (Windows)."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
|
||||||
+14
-4
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -19,7 +23,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
@@ -28,7 +34,11 @@
|
|||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".next/dev/types/**/*.ts",
|
".next/dev/types/**/*.ts",
|
||||||
"**/*.mts"
|
"**/*.mts",
|
||||||
|
".next\\dev/types/**/*.ts",
|
||||||
|
".next\\dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user