Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dappnode_package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"categories": [
"AI"
],
"description": "Run large language models locally on your DAppNode with GPU acceleration. Ollama provides a fast and efficient LLM inference engine with AMD ROCm support.\n\n**Features:**\n- AMD GPU acceleration via ROCm\n- Complete privacy - all processing stays local\n- Support for multiple LLM models (Llama, Mistral, CodeLlama, etc.)\n\n**Requirements:**\n- AMD GPU with ROCm support\n- At least 8GB RAM (16GB+ recommended)\n- Sufficient storage for models (10GB+ recommended)",
"description": "Run large language models locally on your DAppNode with GPU acceleration. Ollama provides a fast and efficient LLM inference engine with AMD ROCm support.\n\n**Features:**\n- AMD GPU acceleration via ROCm\n- Typed decisions with probabilities via the Jev-compatible System One API\n- Complete privacy - all processing stays local\n- Support for multiple LLM models (Llama, Mistral, CodeLlama, etc.)\n\n**Requirements:**\n- AMD GPU with ROCm support\n- At least 8GB RAM (16GB+ recommended)\n- Sufficient storage for models (10GB+ recommended)",
"license": "GPL-3.0",
"links": {
"Models library": "https://ollama.com/library"
Expand All @@ -19,7 +19,7 @@
{
"arg": "OLLAMA_VERSION",
"repo": "ollama/ollama",
"version": "v0.32.5"
"version": "v0.40.11"
}
],
"version": "0.1.0"
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ services:
build:
context: ollama
args:
OLLAMA_VERSION: v0.32.5
OLLAMA_VERSION: v0.40.11
container_name: ollama.ollama.dnp.dappnode.eth
volumes:
- ollama:/root/.ollama
Expand Down
11 changes: 11 additions & 0 deletions getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Now you can:
- Open the **Model Manager** from your package links to browse, pull, and manage your models.
- Browse the full [Ollama model library](https://ollama.com/library) to find models.
- Use the Ollama API to integrate with your apps.
- Run Jev-style decisions locally: choose a label, estimate a yes/no probability, or score text against a rubric.

### Quick start

Expand Down Expand Up @@ -32,6 +33,16 @@ curl http://<ollama-api-url>:11434/api/tags

Point any Ollama-compatible app (Open WebUI, OpenClaw, etc.) to the API URL shown in your package links.

### Decision models (System One)

Use **Decision models only** in Model Manager to filter the catalog. Decision models show a **decision** badge; installed models are identified through their reported capabilities.

Pull a compatible model such as `tev1:0.8b`, `tev1`, `nimble`, `clef`, or `clef-flash`, then use **`POST /v1/systemone`** from your application for choices, yes/no probabilities, and scoring. This is the Jev-compatible API for supported Ollama models; it does not install TypeSafe's hosted Jev model.

The endpoint is available directly at `http://ollama-<variant>.dappnode:11434/v1/systemone` and through Model Manager at `http://ui.ollama-<variant>.dappnode/v1/systemone`. Replace `<variant>` with `cpu`, `amd`, or `nvidia`. Local requests do not require an API key.

See the [decision guide](https://docs.ollama.com/capabilities/decision) and [System One API reference](https://docs.ollama.com/api/systemone) for request formats and limits.

### Resources

- [Ollama documentation](https://github.com/ollama/ollama/blob/main/README.md)
Expand Down
2 changes: 1 addition & 1 deletion package_variants/amd/dappnode_package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"Model Manager": "http://ui.ollama-amd.dappnode",
"api": "http://ollama-amd.dappnode:11434"
},
"description": "Run large language models locally on your DAppNode with GPU acceleration. Ollama with AMD ROCm support for GPU inference.\n\n**Features:**\n- AMD GPU acceleration via ROCm\n- Complete privacy - all processing stays local\n- Support for multiple LLM models (Llama, Mistral, CodeLlama, etc.)\n\n**Requirements:**\n- AMD GPU with ROCm support\n- At least 8GB RAM (16GB+ recommended)\n- Sufficient storage for models (10GB+ recommended)\n"
"description": "Run large language models locally on your DAppNode with GPU acceleration. Ollama with AMD ROCm support for GPU inference.\n\n**Features:**\n- AMD GPU acceleration via ROCm\n- Typed decisions with probabilities via the Jev-compatible System One API\n- Complete privacy - all processing stays local\n- Support for multiple LLM models (Llama, Mistral, CodeLlama, etc.)\n\n**Requirements:**\n- AMD GPU with ROCm support\n- At least 8GB RAM (16GB+ recommended)\n- Sufficient storage for models (10GB+ recommended)\n"
}
11 changes: 7 additions & 4 deletions package_variants/cpu/dappnode_package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{
"name": "ollama-cpu.dnp.dappnode.eth",
"version": "0.1.1",
"architectures": [
"linux/amd64",
"linux/arm64"
],
"description": "Run large language models locally on your DAppNode. Ollama inference engine with CPU-only support.\n\n**Features:**\n- CPU acceleration for inference\n- Typed decisions with probabilities via the Jev-compatible System One API\n- Complete privacy - all processing stays local\n- Support for multiple LLM models (Llama, Mistral, CodeLlama, etc.)\n\n**Requirements:**\n- At least 8GB RAM (16GB+ recommended)\n- Sufficient storage for models (10GB+ recommended)\n",
"links": {
"Model Manager": "http://ui.ollama-cpu.dappnode",
"api": "http://ollama-cpu.dappnode:11434"
},
"architectures": ["linux/amd64", "linux/arm64"],
"description": "Run large language models locally on your DAppNode. Ollama inference engine with CPU-only support.\n\n**Features:**\n- CPU acceleration for inference\n- Complete privacy - all processing stays local\n- Support for multiple LLM models (Llama, Mistral, CodeLlama, etc.)\n\n**Requirements:**\n- At least 8GB RAM (16GB+ recommended)\n- Sufficient storage for models (10GB+ recommended)\n"
"name": "ollama-cpu.dnp.dappnode.eth",
"version": "0.1.2"
}
2 changes: 1 addition & 1 deletion package_variants/nvidia/dappnode_package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"api": "http://ollama-nvidia.dappnode:11434"
},
"architectures": ["linux/amd64", "linux/arm64"],
"description": "Run large language models locally on your DAppNode with GPU acceleration. Ollama with NVIDIA GPU support for GPU inference.\n\n**Features:**\n- NVIDIA GPU acceleration\n- Complete privacy - all processing stays local\n- Support for multiple LLM models (Llama, Mistral, CodeLlama, etc.)\n\n**Requirements:**\n- NVIDIA GPU with CUDA support\n- At least 8GB RAM (16GB+ recommended)\n- Sufficient storage for models (10GB+ recommended)\n"
"description": "Run large language models locally on your DAppNode with GPU acceleration. Ollama with NVIDIA GPU support for GPU inference.\n\n**Features:**\n- NVIDIA GPU acceleration\n- Typed decisions with probabilities via the Jev-compatible System One API\n- Complete privacy - all processing stays local\n- Support for multiple LLM models (Llama, Mistral, CodeLlama, etc.)\n\n**Requirements:**\n- NVIDIA GPU with CUDA support\n- At least 8GB RAM (16GB+ recommended)\n- Sufficient storage for models (10GB+ recommended)\n"
}
128 changes: 97 additions & 31 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,24 @@
opacity: 0.3;
cursor: not-allowed;
}
.decision-badge {
display: inline-block;
font-size: 11px;
font-weight: 500;
color: var(--accent-hover);
background: var(--surface2);
padding: 2px 7px;
border-radius: 5px;
}

.decision-filter {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text2);
margin: 0 0 14px;
}
</style>
</head>

Expand Down Expand Up @@ -691,6 +709,7 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="
<div class="catalog-search">
<input type="text" id="catalogSearch" placeholder="Search models..." />
</div>
<label class="decision-filter"><input type="checkbox" id="decisionCatalogOnly"> Decision models only</label>
<div id="catalogList"></div>
<div class="catalog-pagination" id="catalogPagination"></div>
</div>
Expand All @@ -703,17 +722,22 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="
const API = '/api';
let runningModels = new Set();
let installedModelNames = new Set();
const decisionCapabilities = new Map();
let catalogCache = [];
let catalogQuery = '';
let catalogPage = 1;
const CATALOG_PER_PAGE = 10;
const tagsCacheMap = {}; // modelName -> [{tag, size}]

// --- Init ---
async function init() {
await checkStatus();
await loadModels();
function init() {
checkStatus();
loadModels();
loadCatalog();
document.getElementById('decisionCatalogOnly').addEventListener('change', () => {
catalogPage = 1;
loadCatalog();
});
setInterval(() => { checkStatus(); loadModels(); }, 10000);

let searchTimeout;
Expand All @@ -735,10 +759,11 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="
pagination.innerHTML = '';

try {
const url = catalogQuery
? `/ollama-search?q=${encodeURIComponent(catalogQuery)}`
: '/ollama-search';
const res = await fetch(url);
const params = new URLSearchParams();
if (catalogQuery) params.set('q', catalogQuery);
if (document.getElementById('decisionCatalogOnly').checked) params.set('c', 'decision');
const url = '/ollama-search?' + params.toString();
const res = await fetch(url, { signal: AbortSignal.timeout(10000) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
const parsed = parseCatalogHTML(html);
Expand All @@ -756,6 +781,10 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="

// Fallback curated list when ollama.com is unreachable
const FALLBACK_MODELS = [
{ name: 'tev1', desc: 'Together AI decision models for classification, yes/no probabilities, and scoring', pulls: '', decision: true },
{ name: 'nimble', desc: 'Bespoke Labs 9B model for typed decisions', pulls: '', decision: true },
{ name: 'clef', desc: 'Cloudflare decision model with image input support', pulls: '', decision: true },
{ name: 'clef-flash', desc: 'Compact decision model with image input support', pulls: '', decision: true },
{ name: 'llama3.2', desc: 'Meta Llama 3.2 - lightweight model for diverse tasks', pulls: '' },
{ name: 'llama3.3', desc: 'Meta Llama 3.3 - 70B instruction-tuned model', pulls: '' },
{ name: 'gemma3', desc: 'Google Gemma 3 - lightweight open model', pulls: '' },
Expand All @@ -779,9 +808,11 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="
];

function filterFallback(query) {
if (!query) return FALLBACK_MODELS;
const models = document.getElementById('decisionCatalogOnly').checked
? FALLBACK_MODELS.filter(m => m.decision) : FALLBACK_MODELS;
if (!query) return models;
const q = query.toLowerCase();
return FALLBACK_MODELS.filter(m =>
return models.filter(m =>
m.name.toLowerCase().includes(q) || m.desc.toLowerCase().includes(q)
);
}
Expand Down Expand Up @@ -812,7 +843,7 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="
const pullMatch = allText.match(/([\d.]+[KMB]?\s*Pulls?)/i);
if (pullMatch) pulls = pullMatch[1];

models.push({ name, desc, pulls });
models.push({ name, desc, pulls, decision: /\bdecision\b/i.test(allText) });
});

// Strategy 2: search result IDs
Expand Down Expand Up @@ -861,6 +892,7 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="
<div class="catalog-card-info">
<div class="catalog-card-name">
${esc(m.name)}
${m.decision ? '<span class="decision-badge">decision</span>' : ''}
${isInstalled ? '<span class="installed-check">&#10003;</span>' : ''}
</div>
${m.desc ? `<div class="catalog-card-desc">${esc(m.desc)}</div>` : ''}
Expand Down Expand Up @@ -912,7 +944,9 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="

// Fetch tags from ollama.com
try {
const res = await fetch(`/ollama-library/${encodeURIComponent(modelName)}/tags`);
const res = await fetch(`/ollama-library/${encodeURIComponent(modelName)}/tags`, {
signal: AbortSignal.timeout(10000)
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
const tags = parseTagsHTML(html, modelName);
Expand Down Expand Up @@ -1005,7 +1039,7 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
try {
const res = await fetch(API + '/tags');
const res = await fetch(API + '/tags', { signal: AbortSignal.timeout(5000) });
if (res.ok) {
dot.className = 'dot';
text.textContent = 'Connected';
Expand All @@ -1016,18 +1050,46 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="
}
// Check running models
try {
const res = await fetch(API + '/ps');
const res = await fetch(API + '/ps', { signal: AbortSignal.timeout(5000) });
const data = await res.json();
runningModels = new Set((data.models || []).map(m => m.name));
} catch { runningModels = new Set(); }
}

// Check capabilities once per model digest; retry failed checks on refresh.
async function markDecisionModels(models) {
const cards = document.querySelectorAll('#modelsList .model-card');
await Promise.all(models.map(async (model, index) => {
try {
const key = model.digest || model.name;
if (!decisionCapabilities.has(key)) {
const res = await fetch(API + '/show', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: model.name }),
signal: AbortSignal.timeout(15000)
});
if (!res.ok) return;
const data = await res.json();
decisionCapabilities.set(key, data.capabilities?.includes('decision') === true);
}
if (decisionCapabilities.get(key)) {
const badge = document.createElement('span');
badge.className = 'decision-badge';
badge.textContent = 'decision';
cards[index].querySelector('.model-meta').append(badge);
}
} catch { /* Leave the model usable if its capabilities are unavailable. */ }
}));
}

// --- Load models ---
async function loadModels() {
const list = document.getElementById('modelsList');
const countEl = document.getElementById('modelCount');
try {
const res = await fetch(API + '/tags');
const res = await fetch(API + '/tags', { signal: AbortSignal.timeout(5000) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const models = data.models || [];
installedModelNames = new Set(models.map(m => m.name));
Expand Down Expand Up @@ -1070,6 +1132,7 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="
</div>
</div>`;
}).join('');
markDecisionModels(models);
} catch {
list.innerHTML = `
<div class="empty-state">
Expand Down Expand Up @@ -1098,10 +1161,27 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, stream: true })
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `HTTP ${res.status}`);
}

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let succeeded = false;
function processLine(line) {
if (!line.trim()) return;
const json = JSON.parse(line);
if (json.error) throw new Error(json.error);
if (json.status === 'success') succeeded = true;
if (json.total && json.completed) {
const pct = Math.round((json.completed / json.total) * 100);
updateProgress(json.status || 'Downloading...', pct);
} else {
updateProgress(json.status || 'Processing...', -1);
}
}

while (true) {
const { done, value } = await reader.read();
Expand All @@ -1111,24 +1191,10 @@ <h2>Available models <a href="https://ollama.com/search" target="_blank" style="
const lines = buffer.split('\n');
buffer = lines.pop();

for (const line of lines) {
if (!line.trim()) continue;
try {
const json = JSON.parse(line);
if (json.error) {
throw new Error(json.error);
}
if (json.total && json.completed) {
const pct = Math.round((json.completed / json.total) * 100);
updateProgress(json.status || 'Downloading...', pct);
} else {
updateProgress(json.status || 'Processing...', -1);
}
} catch (e) {
if (e.message && !e.message.includes('JSON')) throw e;
}
}
for (const line of lines) processLine(line);
}
processLine(buffer + decoder.decode());
if (!succeeded) throw new Error('Download ended before Ollama confirmed success. Please retry.');

toast(`Model "${name}" pulled successfully`, 'success');
input.value = '';
Expand Down
19 changes: 18 additions & 1 deletion ui/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ server {
root /usr/share/nginx/html;
index index.html;

# Resolve at request time so UI startup and backend recreation are independent.
resolver 127.0.0.11 valid=5s ipv6=off;
resolver_timeout 3s;
set $ollama_backend ollama:11434;

# Serve the UI
location / {
try_files $uri $uri/ /index.html;
Expand All @@ -31,8 +36,20 @@ server {
}

# Proxy Ollama API requests
# System One and other OpenAI-compatible endpoints use /v1/, not /api/.
location /v1/ {
proxy_pass http://$ollama_backend$request_uri;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 600s;
proxy_connect_timeout 60s;
client_max_body_size 32m;
}

location /api/ {
proxy_pass http://ollama:11434/api/;
proxy_pass http://$ollama_backend$request_uri;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
Expand Down
Loading
Loading