diff --git a/client/src/components/Admin/AddonManager.test.tsx b/client/src/components/Admin/AddonManager.test.tsx
index 6be9a96ad..ad5f1ccb0 100644
--- a/client/src/components/Admin/AddonManager.test.tsx
+++ b/client/src/components/Admin/AddonManager.test.tsx
@@ -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 () => {
const user = userEvent.setup();
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();
await screen.findByText('Installed on the server');
@@ -483,9 +483,9 @@ describe('AddonManager', () => {
await user.click(screen.getByRole('button', { name: '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' }));
- 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();
});
@@ -545,7 +545,7 @@ describe('AddonManager', () => {
addonsRoute([llmAddon({ provider: 'local' })]),
http.get('/api/admin/llm/local/models', () => {
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 }) => {
pulled = await request.json();
@@ -565,8 +565,8 @@ describe('AddonManager', () => {
expect(screen.getByText('starting…')).toBeInTheDocument();
await screen.findByText('Model pulled');
- expect(pulled).toEqual({ baseUrl: 'http://localhost:11434/v1', model: 'qwen3:8b' });
- expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('qwen3:8b');
+ expect(pulled).toEqual({ baseUrl: 'http://localhost:11434/v1', model: 'qwen3.5:4b' });
+ expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('qwen3.5:4b');
await waitFor(() => expect(screen.getByRole('button', { name: 'Selected' })).toBeDisabled());
});
diff --git a/client/src/components/Admin/AddonManager.tsx b/client/src/components/Admin/AddonManager.tsx
index 05010f7dd..e4562a492 100644
--- a/client/src/components/Admin/AddonManager.tsx
+++ b/client/src/components/Admin/AddonManager.tsx
@@ -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
* 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 }[] = [
- { 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 },
]
/**
diff --git a/client/src/mobile/screens/admin/MAdminAddonManager.tsx b/client/src/mobile/screens/admin/MAdminAddonManager.tsx
index dd5cba793..de3f4cc6e 100644
--- a/client/src/mobile/screens/admin/MAdminAddonManager.tsx
+++ b/client/src/mobile/screens/admin/MAdminAddonManager.tsx
@@ -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
* 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 }[] = [
- { 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 },
]
/**
diff --git a/client/tests/unit/mobile/admin/MAdminAddonManager.test.tsx b/client/tests/unit/mobile/admin/MAdminAddonManager.test.tsx
index ab2cc5eaa..96bbb29fe 100644
--- a/client/tests/unit/mobile/admin/MAdminAddonManager.test.tsx
+++ b/client/tests/unit/mobile/admin/MAdminAddonManager.test.tsx
@@ -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 () => {
const user = userEvent.setup();
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();
await screen.findByText('Installed on the server');
@@ -378,9 +378,9 @@ describe('MAdminAddonManager', () => {
await user.click(screen.getByRole('button', { name: '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' }));
- 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();
});
@@ -449,7 +449,7 @@ describe('MAdminAddonManager', () => {
addonsRoute([llmAddon({ provider: 'local' })]),
http.get('/api/admin/llm/local/models', () => {
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 }) => {
pulled = await request.json();
@@ -470,8 +470,8 @@ describe('MAdminAddonManager', () => {
expect(screen.getByText('starting…')).toBeInTheDocument();
await screen.findByText('Model pulled');
- expect(pulled).toEqual({ baseUrl: 'http://localhost:11434/v1', model: 'qwen3:8b' });
- expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('qwen3:8b');
+ expect(pulled).toEqual({ baseUrl: 'http://localhost:11434/v1', model: 'qwen3.5:4b' });
+ expect(screen.getByPlaceholderText('select or pull below')).toHaveValue('qwen3.5:4b');
// Reloaded models now contain the pulled one, so the row switches to "Selected".
await waitFor(() => expect(screen.getByRole('button', { name: 'Selected' })).toBeDisabled());
});
diff --git a/wiki/AI-Booking-Import.md b/wiki/AI-Booking-Import.md
index eb417ef8d..846e0bb5e 100644
--- a/wiki/AI-Booking-Import.md
+++ b/wiki/AI-Booking-Import.md
@@ -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.
- **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.
-- **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).
@@ -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:
- **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.