mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(admin): recommend the local model that also reads images
The one model the addon offers to pull was Qwen3 8B, which is text-only, so a scanned page or a photographed document reached it as no text at all and came back empty. Qwen3.5 4B is multimodal at a third of the size: one download covers every kind of document instead of leaving the operator to work out that the recommended model cannot read half of them. The list stays one entry long. A self-hosted host needs a model, not a menu, and any other id can still be typed in or picked from what Ollama already has.
This commit is contained in:
@@ -473,7 +473,7 @@ describe('AddonManager', () => {
|
|||||||
it('FE-ADMIN-ADDON-021: the local provider lists installed models and a chip fills the model field', async () => {
|
it('FE-ADMIN-ADDON-021: the local provider lists installed models and a chip fills the model field', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const urls: (string | null)[] = [];
|
const urls: (string | null)[] = [];
|
||||||
server.use(addonsRoute([llmAddon({ provider: 'local' })]), modelsRoute(['qwen3:8b', 'llama3:8b'], urls));
|
server.use(addonsRoute([llmAddon({ provider: 'local' })]), modelsRoute(['qwen3.5:4b', 'llama3:8b'], urls));
|
||||||
render(<AddonManager />);
|
render(<AddonManager />);
|
||||||
|
|
||||||
await screen.findByText('Installed on the server');
|
await screen.findByText('Installed on the server');
|
||||||
@@ -483,9 +483,9 @@ describe('AddonManager', () => {
|
|||||||
await user.click(screen.getByRole('button', { name: 'llama3:8b' }));
|
await user.click(screen.getByRole('button', { name: 'llama3:8b' }));
|
||||||
expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('llama3:8b');
|
expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('llama3:8b');
|
||||||
|
|
||||||
// qwen3:8b is already installed, so the recommended row offers "Use" instead of "Pull"
|
// qwen3.5:4b is already installed, so the recommended row offers "Use" instead of "Pull"
|
||||||
await user.click(screen.getByRole('button', { name: 'Use' }));
|
await user.click(screen.getByRole('button', { name: 'Use' }));
|
||||||
expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('qwen3:8b');
|
expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('qwen3.5:4b');
|
||||||
expect(screen.getByRole('button', { name: 'Selected' })).toBeDisabled();
|
expect(screen.getByRole('button', { name: 'Selected' })).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -545,7 +545,7 @@ describe('AddonManager', () => {
|
|||||||
addonsRoute([llmAddon({ provider: 'local' })]),
|
addonsRoute([llmAddon({ provider: 'local' })]),
|
||||||
http.get('/api/admin/llm/local/models', () => {
|
http.get('/api/admin/llm/local/models', () => {
|
||||||
modelCalls += 1;
|
modelCalls += 1;
|
||||||
return HttpResponse.json({ models: modelCalls === 1 ? [] : [{ name: 'qwen3:8b', size: 1 }] });
|
return HttpResponse.json({ models: modelCalls === 1 ? [] : [{ name: 'qwen3.5:4b', size: 1 }] });
|
||||||
}),
|
}),
|
||||||
http.post('/api/admin/llm/local/pull', async ({ request }) => {
|
http.post('/api/admin/llm/local/pull', async ({ request }) => {
|
||||||
pulled = await request.json();
|
pulled = await request.json();
|
||||||
@@ -565,8 +565,8 @@ describe('AddonManager', () => {
|
|||||||
expect(screen.getByText('starting…')).toBeInTheDocument();
|
expect(screen.getByText('starting…')).toBeInTheDocument();
|
||||||
|
|
||||||
await screen.findByText('Model pulled');
|
await screen.findByText('Model pulled');
|
||||||
expect(pulled).toEqual({ baseUrl: 'http://localhost:11434/v1', model: 'qwen3:8b' });
|
expect(pulled).toEqual({ baseUrl: 'http://localhost:11434/v1', model: 'qwen3.5:4b' });
|
||||||
expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('qwen3:8b');
|
expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('qwen3.5:4b');
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: 'Selected' })).toBeDisabled());
|
await waitFor(() => expect(screen.getByRole('button', { name: 'Selected' })).toBeDisabled());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -306,9 +306,10 @@ const DEFAULT_OLLAMA_URL = 'http://localhost:11434/v1'
|
|||||||
|
|
||||||
/** Curated models the local extractor is tuned for, pullable via Ollama. The router drives
|
/** Curated models the local extractor is tuned for, pullable via Ollama. The router drives
|
||||||
* one model per document via Ollama's grammar-constrained `format`; "thinking" is disabled
|
* one model per document via Ollama's grammar-constrained `format`; "thinking" is disabled
|
||||||
* automatically, so the Qwen3 family works without any tuning. A host only needs one. */
|
* automatically, so the Qwen3 family works without any tuning. A host only needs one — and
|
||||||
|
* Qwen3.5 reads images as well as text, so that one covers every kind of document. */
|
||||||
const RECOMMENDED_MODELS: { id: string; label: string; note: string; recommended: boolean; vision: boolean }[] = [
|
const RECOMMENDED_MODELS: { id: string; label: string; note: string; recommended: boolean; vision: boolean }[] = [
|
||||||
{ id: 'qwen3:8b', label: 'Qwen3 — 8B', note: 'Recommended · best extraction quality & speed on CPU (thinking auto-disabled) · Apache-2.0', recommended: true, vision: false },
|
{ id: 'qwen3.5:4b', label: 'Qwen3.5 — 4B', note: 'Recommended · reads images as well as text, so one model covers every document (3.4 GB, 256K context, thinking auto-disabled)', recommended: true, vision: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -351,9 +351,10 @@ const DEFAULT_OLLAMA_URL = 'http://localhost:11434/v1'
|
|||||||
|
|
||||||
/** Curated models the local extractor is tuned for, pullable via Ollama. The router drives
|
/** Curated models the local extractor is tuned for, pullable via Ollama. The router drives
|
||||||
* one model per document via Ollama's grammar-constrained `format`; "thinking" is disabled
|
* one model per document via Ollama's grammar-constrained `format`; "thinking" is disabled
|
||||||
* automatically, so the Qwen3 family works without any tuning. A host only needs one. */
|
* automatically, so the Qwen3 family works without any tuning. A host only needs one — and
|
||||||
|
* Qwen3.5 reads images as well as text, so that one covers every kind of document. */
|
||||||
const RECOMMENDED_MODELS: { id: string; label: string; note: string; recommended: boolean; vision: boolean }[] = [
|
const RECOMMENDED_MODELS: { id: string; label: string; note: string; recommended: boolean; vision: boolean }[] = [
|
||||||
{ id: 'qwen3:8b', label: 'Qwen3 — 8B', note: 'Recommended · best extraction quality & speed on CPU (thinking auto-disabled) · Apache-2.0', recommended: true, vision: false },
|
{ id: 'qwen3.5:4b', label: 'Qwen3.5 — 4B', note: 'Recommended · reads images as well as text, so one model covers every document (3.4 GB, 256K context, thinking auto-disabled)', recommended: true, vision: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -368,7 +368,7 @@ describe('MAdminAddonManager', () => {
|
|||||||
it('FE-MOB-AADD-017: the local provider lists installed models and selecting a chip fills the model field', async () => {
|
it('FE-MOB-AADD-017: the local provider lists installed models and selecting a chip fills the model field', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const urls: (string | null)[] = [];
|
const urls: (string | null)[] = [];
|
||||||
server.use(addonsRoute([llmAddon({ provider: 'local' })]), modelsRoute(['qwen3:8b', 'llama3:8b'], urls));
|
server.use(addonsRoute([llmAddon({ provider: 'local' })]), modelsRoute(['qwen3.5:4b', 'llama3:8b'], urls));
|
||||||
render(<MAdminAddonManager />);
|
render(<MAdminAddonManager />);
|
||||||
|
|
||||||
await screen.findByText('Installed on the server');
|
await screen.findByText('Installed on the server');
|
||||||
@@ -378,9 +378,9 @@ describe('MAdminAddonManager', () => {
|
|||||||
await user.click(screen.getByRole('button', { name: 'llama3:8b' }));
|
await user.click(screen.getByRole('button', { name: 'llama3:8b' }));
|
||||||
expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('llama3:8b');
|
expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('llama3:8b');
|
||||||
|
|
||||||
// qwen3:8b is installed, so the recommended row offers "Use" instead of "Pull".
|
// qwen3.5:4b is installed, so the recommended row offers "Use" instead of "Pull".
|
||||||
await user.click(screen.getByRole('button', { name: 'Use' }));
|
await user.click(screen.getByRole('button', { name: 'Use' }));
|
||||||
expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('qwen3:8b');
|
expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('qwen3.5:4b');
|
||||||
expect(screen.getByRole('button', { name: 'Selected' })).toBeDisabled();
|
expect(screen.getByRole('button', { name: 'Selected' })).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -449,7 +449,7 @@ describe('MAdminAddonManager', () => {
|
|||||||
addonsRoute([llmAddon({ provider: 'local' })]),
|
addonsRoute([llmAddon({ provider: 'local' })]),
|
||||||
http.get('/api/admin/llm/local/models', () => {
|
http.get('/api/admin/llm/local/models', () => {
|
||||||
modelCalls += 1;
|
modelCalls += 1;
|
||||||
return HttpResponse.json({ models: modelCalls === 1 ? [] : [{ name: 'qwen3:8b', size: 1 }] });
|
return HttpResponse.json({ models: modelCalls === 1 ? [] : [{ name: 'qwen3.5:4b', size: 1 }] });
|
||||||
}),
|
}),
|
||||||
http.post('/api/admin/llm/local/pull', async ({ request }) => {
|
http.post('/api/admin/llm/local/pull', async ({ request }) => {
|
||||||
pulled = await request.json();
|
pulled = await request.json();
|
||||||
@@ -470,8 +470,8 @@ describe('MAdminAddonManager', () => {
|
|||||||
expect(screen.getByText('starting…')).toBeInTheDocument();
|
expect(screen.getByText('starting…')).toBeInTheDocument();
|
||||||
|
|
||||||
await screen.findByText('Model pulled');
|
await screen.findByText('Model pulled');
|
||||||
expect(pulled).toEqual({ baseUrl: 'http://localhost:11434/v1', model: 'qwen3:8b' });
|
expect(pulled).toEqual({ baseUrl: 'http://localhost:11434/v1', model: 'qwen3.5:4b' });
|
||||||
expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('qwen3:8b');
|
expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('qwen3.5:4b');
|
||||||
// Reloaded models now contain the pulled one, so the row switches to "Selected".
|
// Reloaded models now contain the pulled one, so the row switches to "Selected".
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: 'Selected' })).toBeDisabled());
|
await waitFor(() => expect(screen.getByRole('button', { name: 'Selected' })).toBeDisabled());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ When you enable the addon, a configuration panel appears directly under it in [A
|
|||||||
- **Provider** — Local · OpenAI-compatible, OpenAI, or Anthropic.
|
- **Provider** — Local · OpenAI-compatible, OpenAI, or Anthropic.
|
||||||
- **Base URL** — shown for every provider except Anthropic. Defaults to `http://localhost:11434/v1` for a local Ollama server, or `https://api.openai.com/v1` for OpenAI. Point it at any OpenAI-compatible endpoint here.
|
- **Base URL** — shown for every provider except Anthropic. Defaults to `http://localhost:11434/v1` for a local Ollama server, or `https://api.openai.com/v1` for OpenAI. Point it at any OpenAI-compatible endpoint here.
|
||||||
- **API key** — optional for a local server (`(often not required)`), required for the cloud providers. Stored **encrypted**; it is shown masked (`••••••••`) once saved, and leaving it unchanged keeps the stored key.
|
- **API key** — optional for a local server (`(often not required)`), required for the cloud providers. Stored **encrypted**; it is shown masked (`••••••••`) once saved, and leaving it unchanged keeps the stored key.
|
||||||
- **Model** — the model id (e.g. `qwen3:8b`, `gpt-4o`, `claude-opus-4-8`).
|
- **Model** — the model id (e.g. `qwen3.5:4b`, `gpt-4o`, `claude-opus-4-8`).
|
||||||
|
|
||||||
If you set a provider and model here, it applies to **all users** and overrides their personal settings. Leave the panel blank to let each user bring their own model (see below).
|
If you set a provider and model here, it applies to **all users** and overrides their personal settings. Leave the panel blank to let each user bring their own model (see below).
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ If you set a provider and model here, it applies to **all users** and overrides
|
|||||||
With the **Local** provider selected, the panel manages your Ollama server directly:
|
With the **Local** provider selected, the panel manages your Ollama server directly:
|
||||||
|
|
||||||
- **Installed on the server** lists the models Ollama already has, with a **Refresh** button. Click a model to select it.
|
- **Installed on the server** lists the models Ollama already has, with a **Refresh** button. Click a model to select it.
|
||||||
- **Pull a recommended model** downloads a model with a live progress bar. The one recommended model is **Qwen3 — 8B** (`qwen3:8b`) — *best extraction quality & speed on CPU (thinking auto-disabled) · Apache-2.0*. Once the pull finishes it is selected automatically.
|
- **Pull a recommended model** downloads a model with a live progress bar. The one recommended model is **Qwen3.5 — 4B** (`qwen3.5:4b`, 3.4 GB, 256K context) — it reads images as well as text, so a single download covers every kind of document, and "thinking" is disabled automatically. Once the pull finishes it is selected automatically.
|
||||||
|
|
||||||
You can also select any other model already installed on the server, or type a model id by hand.
|
You can also select any other model already installed on the server, or type a model id by hand.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user