fix: reset hidden aiProvider when switching to manual AI configuration - #238
Open
admonstrator wants to merge 1 commit into
Open
fix: reset hidden aiProvider when switching to manual AI configuration#238admonstrator wants to merge 1 commit into
admonstrator wants to merge 1 commit into
Conversation
Background: Selecting a named AI provider preset (e.g. "OpenAI (ChatGPT)") in Step 5 of the setup wizard sets the hidden `aiProvider` input to that preset's provider. Switching back to "Manual custom configuration" only updated the hint text and left the stale provider value in place. Clicking "Test AI connection" then sent the stale provider (e.g. `openai`) alongside the user's custom URL/key/model. The backend dispatches purely on `aiProvider`, so `validateOpenAIConfig` ran with no `baseURL` and always hit api.openai.com, ignoring the custom endpoint entirely and returning a confusing 401 that references OpenAI's own docs. Changes: - `public/js/setup.js` `applyPreset()`: reset `aiProvider.value` to `'custom'` in the `if (!preset)` branch (manual mode) before updating the hint text. - Also cleaned up three pre-existing lint issues in the same file so the full file passes ESLint per this repo's changed-files CI gate: a missing `/* global Swal */` directive (SweetAlert2 is loaded via a script tag, not a standard global), an unused `catch (_error)` binding, and a stale `eslint-disable-next-line no-await-in-loop` comment for a rule that isn't enabled in this config. Testing: - Added `tests/test-setup-preset-manual-reset.js`: selects a named preset, then applies `null` (manual mode), and asserts `aiProvider` resets to `custom`. Registered in `scripts/run-tests.js`. - `node scripts/run-tests.js --all`: 43 passed, 7 skipped (server-dependent), 0 failed. - `npx eslint public/js/setup.js tests/test-setup-preset-manual-reset.js scripts/run-tests.js`: clean. Note: `npx prettier --check public/js/setup.js` still fails — this file predates the repo's Prettier/ESLint CI gate (added in 918801b, after this file's last edit) and has never been reformatted. A full reformat would touch ~4000 unrelated lines, so it was intentionally left out of this focused bugfix per discussion with the repo maintainer; a separate repo-wide formatting pass is a better fit for that. Impact: Any user who explores the preset dropdown and then switches to manual configuration for a custom OpenAI-compatible endpoint (Mistral, OpenRouter, DeepSeek, self-hosted vLLM, etc.) will now have the AI connection test actually exercise their configured endpoint instead of silently falling back to OpenAI's default endpoint. Upstream Status: new fix, not yet upstreamed to clusterzx/paperless-ai. Closes #235
This was referenced Jul 18, 2026
admonstrator
added a commit
that referenced
this pull request
Jul 18, 2026
* fix: reset hidden aiProvider when switching to manual AI configuration Background: Selecting a named AI provider preset (e.g. "OpenAI (ChatGPT)") in Step 5 of the setup wizard sets the hidden `aiProvider` input to that preset's provider. Switching back to "Manual custom configuration" only updated the hint text and left the stale provider value in place. Clicking "Test AI connection" then sent the stale provider (e.g. `openai`) alongside the user's custom URL/key/model. The backend dispatches purely on `aiProvider`, so `validateOpenAIConfig` ran with no `baseURL` and always hit api.openai.com, ignoring the custom endpoint entirely and returning a confusing 401 that references OpenAI's own docs. Changes: - `public/js/setup.js` `applyPreset()`: reset `aiProvider.value` to `'custom'` in the `if (!preset)` branch (manual mode) before updating the hint text. - Also cleaned up three pre-existing lint issues in the same file so the full file passes ESLint per this repo's changed-files CI gate: a missing `/* global Swal */` directive (SweetAlert2 is loaded via a script tag, not a standard global), an unused `catch (_error)` binding, and a stale `eslint-disable-next-line no-await-in-loop` comment for a rule that isn't enabled in this config. Testing: - Added `tests/test-setup-preset-manual-reset.js`: selects a named preset, then applies `null` (manual mode), and asserts `aiProvider` resets to `custom`. Registered in `scripts/run-tests.js`. - `node scripts/run-tests.js --all`: 43 passed, 7 skipped (server-dependent), 0 failed. - `npx eslint public/js/setup.js tests/test-setup-preset-manual-reset.js scripts/run-tests.js`: clean. Note: `npx prettier --check public/js/setup.js` still fails — this file predates the repo's Prettier/ESLint CI gate (added in 918801b, after this file's last edit) and has never been reformatted. A full reformat would touch ~4000 unrelated lines, so it was intentionally left out of this focused bugfix per discussion with the repo maintainer; a separate repo-wide formatting pass is a better fit for that. Impact: Any user who explores the preset dropdown and then switches to manual configuration for a custom OpenAI-compatible endpoint (Mistral, OpenRouter, DeepSeek, self-hosted vLLM, etc.) will now have the AI connection test actually exercise their configured endpoint instead of silently falling back to OpenAI's default endpoint. Upstream Status: new fix, not yet upstreamed to clusterzx/paperless-ai. Closes #235 * fix: resolve AI response/prompt log paths relative to process.cwd() Background: services/openaiService.js, customService.js, and azureService.js each hardcode `path.join('/app', 'data', 'logs', 'response.txt')` for AI response logging, and services/serviceUtils.js's writePromptToFile() defaults its filePath parameter to the literal '/app/data/logs/prompt.txt'. These paths only exist inside the Docker image (Dockerfile.base sets WORKDIR /app). On a native/bare-metal install — explicitly documented as supported in the README, e.g. a systemd service or LXC container running from /opt/paperless-ai-next — process.cwd() is the app directory, not /app, so fs.mkdir('/app', ...) fails with ENOENT on every single document processed. The error is caught and logged as a warning, so processing still completes, but response.txt and prompt.txt are silently never written, and the journal fills up with the same repeated warning. Changes: - services/openaiService.js, customService.js, azureService.js: replaced the hardcoded `/app` segment with `process.cwd()` in the responseLogPath constant. - services/serviceUtils.js: writePromptToFile()'s default filePath now resolves to `path.join(process.cwd(), 'data', 'logs', 'prompt.txt')`. - This matches how config/config.js, models/document.js, and the rest of the codebase already resolve data/ paths, so Docker behavior (WORKDIR /app) is unchanged. Testing: - Added tests/test-native-install-log-paths.js: a static source-text check (fs.readFileSync + string assertions, no app boot required, following the existing convention in tests/test-history-xss-hardening.js) asserting none of the four files hardcode '/app' for these log paths and all resolve via process.cwd(). Registered in scripts/run-tests.js (observability area). - node scripts/run-tests.js --all: 43 passed, 7 skipped (server-dependent), 0 failed. - npx eslint/prettier clean on the new test file and scripts/run-tests.js. - node scripts/regen-openapi.js produces no diff (no API surface change). Note: openaiService.js, customService.js, and azureService.js already fail `npx eslint` with 16 pre-existing errors unrelated to this change (unused imports/vars, and a `preserve-caught-error` rule about missing `cause` on rethrown errors) — confirmed present on origin/main before this commit. Per discussion with the repo maintainer, those are left untouched here since fixing the `cause`-chain violations would mean touching real error-handling logic across all three files, well outside this fix's scope; a separate cleanup PR is a better fit. Impact: Native/bare-metal installs (systemd, LXC, etc.) now get working AI response and prompt logs, matching Docker behavior, and stop spamming the journal with the same mkdir warning on every processed document. Upstream Status: new fix, not yet upstreamed to clusterzx/paperless-ai. Closes #237 * fix: stop gating Quickstart's OCR model dropdown on the vision heuristic Background: Quickstart auto-detect classifies models by name heuristics only (services/quickstartService.js classifyModelName()), and the "Suggested OCR model" dropdown/checkbox in Step 5 was gated entirely on the resulting `visionModels` list. A dedicated OCR model with an unfamiliar name - e.g. Mistral's `mistral-ocr-latest` - matches none of the hardcoded vision hints (llava, pixtral, vision, gemma3, ...), so it classifies as `['text']` and never appears in that list. The dropdown showed "No vision-capable models found" and the "enable OCR" checkbox was disabled, even though the model exists and works - forcing users into manual configuration for any provider whose naming doesn't match the hint list. detectAndClassify() also always returned `ocrProvider: 'custom'`, never the `'mistral'` OCR provider that manual setup already fully supports (services/setupService.js validateOcrConfig, and the "Mistral OCR API" option in the always-visible "OCR fallback" wizard step). We considered parsing the `capabilities` object some OpenAI-compatible `/v1/models` responses include (Mistral does; LM Studio, Ollama, and plain OpenAI-compatible servers don't) to classify models more accurately, but rejected it: that's an undocumented, vendor-specific extension, not part of the OpenAI /v1/models spec, and would only special-case one provider while leaving every other naming scheme on the same guesswork - the actual underlying problem. Changes: - services/quickstartService.js: added `resolveOcrProviderDefault(url)`, a pure host-string check (mirrors the existing api.mistral.ai check in setupService.getMistralUrlValidationOptions) used in detectAndClassify() to default `ocrProvider` to `'mistral'` when the detected host is api.mistral.ai, `'custom'` otherwise. No classification logic changed - classifyModelName/classifyLmStudioEntry/classifyOllamaShowPayload are untouched. - public/js/setup.js runQuickstartDetect(): the OCR dropdown and "enable OCR" checkbox are now driven by the same non-embedding candidate list as the AI dropdown (`textModels` - every model is already classified as exactly one of ['embedding'] / ['text'] / ['text','vision'], so "has text capability" already means "not embedding-only"), instead of the vision-heuristic-filtered `visionModels`. A heuristic `suggestedOcrModel` is still pre-selected when available; when it isn't, the user picks from the full list themselves rather than seeing an empty/disabled dropdown. - public/js/setup.js applyQuickstartToManualFields(): uses `detection.ocrProvider` (the host-based default above) instead of a hardcoded `'custom'` literal. - views/setup.ejs: updated the dropdown label from "Suggested OCR model (vision-capable)" to "OCR model", since it's no longer filtered to heuristically vision-classified models. - A wrong provider default is never a dead end: the "OCR fallback" wizard step (its own always-visible step, not nested under the Quickstart/Manual AI toggle) lets the user change the OCR provider dropdown themselves before finishing setup regardless. - Cleaned up the same three pre-existing ESLint issues in setup.js as PR #238 (missing `/* global Swal */`, unused `catch (_error)` binding, stale `no-await-in-loop` disable comment) so the full file passes this repo's changed-files ESLint gate. Testing: - tests/test-quickstart-model-classification.js: added classifyModelName('mistral-ocr-latest') === ['text'] (documents why the OCR dropdown must not gate on the vision heuristic) and resolveOcrProviderDefault() cases for a Mistral host, a versioned Mistral URL, a local/non-Mistral host, and a blank host. - tests/test-setup-wizard-quickstart.js: added a second detection fixture (openai-compatible, api.mistral.ai, a model present only in textModels) asserting the OCR dropdown/checkbox stay enabled, manually selecting the dedicated OCR model flows through to the manual OCR model field, and the OCR provider defaults to 'mistral'. Verified this new assertion actually fails without the fix (reverted the source changes locally, confirmed the test catches the regression, then restored). - node scripts/run-tests.js --all: 42 passed, 7 skipped (server-dependent), 0 failed. - npx eslint clean on all changed JS files. - node scripts/regen-openapi.js produces no diff (no API surface change - the quickstart/detect response gains no new field, `ocrProvider`'s value just changes). Note: as in PR #238/#239, npx prettier --check still fails on setup.js/quickstartService.js/the two test files - all pre-existing formatting drift confirmed present on origin/main before this change, intentionally left untouched per the same discussion with the repo maintainer. Impact: Any OpenAI-compatible provider whose model names don't match the hardcoded vision hints (Mistral's OCR models, and any future differently-named dedicated OCR/vision model from any provider) can now be selected as the OCR model through Quickstart instead of forcing manual configuration. Detecting Mistral's own API additionally pre-selects the dedicated Mistral OCR provider path instead of the generic chat-completions path, which doesn't work for Mistral's non-chat OCR models (`completion_chat: false`). Upstream Status: new fix, not yet upstreamed to clusterzx/paperless-ai. Closes #236 * fix: suggest dedicated OCR models and fix Settings quickstart parity Background: PR #236 fixed the Setup Wizard's Quickstart OCR model dropdown to stop gating on the vision name-heuristic, since dedicated OCR models (e.g. Mistral's mistral-ocr-latest) classify as plain ['text'] and never matched the hardcoded vision hints. Two gaps remained: 1. quickstartService.suggestModels() still only considered visionCandidates when computing suggestedOcrModel, so even with the dropdown showing every text-capable model, nothing was ever pre-selected for hosts like api.mistral.ai - the dropdown looked empty/unhelpful in practice even though it wasn't. 2. The Settings page (views/settings.ejs / public/js/settings.js) has its own, separate copy of the Quickstart detect-and-apply flow that PR #236 never touched. It still filtered the OCR dropdown by visionModels ("No vision-capable models found") and hardcoded ocrProvider to 'custom' when applying quickstart results to the manual OCR fields - the exact bugs #236 fixed in the Setup Wizard. Changes: - services/quickstartService.js: added ocrNameHints (['ocr']) and ocrNameCandidates in suggestModels(); OCR-named text models now take priority over generic vision models when suggesting suggestedOcrModel, instead of only ever considering visionCandidates. - public/js/settings.js: quickstart OCR dropdown/checkbox now driven by textModels (aliased ocrCandidateModels) instead of visionModels, mirroring public/js/setup.js. Applying quickstart results to the manual OCR fields now uses quickstartDetection.ocrProvider instead of a hardcoded 'custom' literal. - views/settings.ejs: updated the OCR dropdown label from "Suggested OCR model (vision-capable)" to "OCR model", matching setup.ejs. Testing: - tests/test-quickstart-model-classification.js: added suggestModels() cases for a dedicated OCR-named model with no vision classification, and for an OCR-named model taking priority over a generic vision model. - node scripts/run-tests.js --all: 47 passed, 3 skipped (server-dependent), 1 failed (rate-limiting - pre-existing, server-dependent, unrelated to this change). - npx eslint clean on all changed JS files. - node scripts/regen-openapi.js produces no diff. Impact: Mistral (and any other provider with dedicated, non-vision-named OCR models) now gets a correct default OCR model suggestion in both the Setup Wizard and the Settings page's Quickstart flow, and the OCR provider defaults to 'mistral' instead of 'custom' in both places when the detected host is api.mistral.ai. Upstream Status: new fix, not yet upstreamed to clusterzx/paperless-ai. * polish: turn Quickstart's OCR checkbox into a real toggle switch Background: The Quickstart "also configure OCR" checkbox was a bare <input type="checkbox"> + text label pair, inconsistent with the rest of the settings UI, which already has an established on/off toggle visual (see externalApiEnabled in views/settings.ejs) using a sr-only-peer checkbox driving a pill-shaped slider via Tailwind peer-checked/peer-disabled variants. Changes: - views/setup.ejs and views/settings.ejs: quickstartEnableOcr / settingsQuickstartEnableOcr now render as that same toggle-switch pattern instead of a plain checkbox. Element ids are unchanged, so the existing JS (public/js/setup.js, public/js/settings.js) that reads .checked/.disabled on these elements needed no changes. - Relabeled from "Also configure OCR fallback with this endpoint" to "Use this service for OCR" (shorter, matches how the rest of the Quickstart panel refers to "this" detected endpoint). Testing: - npx eslint clean (views/*.ejs are outside ESLint's scope; the two JS files that read these elements are unaffected). - node scripts/run-tests.js --test setup-wizard-quickstart: passed. - Verified rendering by starting the dev server and curling http://localhost:3000/setup - the toggle markup renders as written, no EJS errors. /settings sits behind auth so it wasn't curled directly, but its markup is identical to the verified setup.ejs block. Impact: Purely visual/label change; no behavior change to the Quickstart detect-and-apply flow itself. Upstream Status: new fix, not yet upstreamed to clusterzx/paperless-ai. * chore: bump version to v2026.07.04 Background: The branch now includes PR #235 (setup preset aiProvider reset), #236 (Quickstart OCR model detection/suggestion, plus the follow-up fixes for the Settings page parity and the OCR toggle switch), and #237 (native install log paths). Bumping PAPERLESS_AI_VERSION and adding the matching changelog entry per the convention in config/changelog.js. Changes: - config/config.js: PAPERLESS_AI_VERSION 'v2026.07.03' -> 'v2026.07.04'. - config/changelog.js: added the v2026.07.04 release block summarizing the OCR/Quickstart fixes, the AI provider reset fix, the log path fix, and the OCR toggle switch polish. Testing: - node -e "require('./config/changelog.js')" to confirm the module still loads/exports correctly. - npx eslint clean on both changed files. - node scripts/run-tests.js --all: 44 passed, 7 skipped (server not running), 0 failed. Upstream Status: version bump specific to this fork, not applicable upstream. --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
aiProviderinput to that preset's provider value.openai) alongside the user's custom URL/key/model. The backend dispatches purely onaiProvider, sovalidateOpenAIConfigran with nobaseURLand always hitapi.openai.com, ignoring the custom endpoint and returning a misleading 401 that references OpenAI's own docs.applyPreset()'sif (!preset)branch (manual mode) now resetsaiProvider.valueto'custom'before updating the hint text./* global Swal */directive, an unusedcatch (_error)binding, a staleeslint-disable-next-line no-await-in-loopcomment) so the full file passes this repo's changed-files ESLint gate.Test plan
tests/test-setup-preset-manual-reset.js: selects a named preset, then appliesnull(manual mode), assertsaiProviderresets tocustom. Registered inscripts/run-tests.js(quickstartarea).node scripts/run-tests.js --all: 43 passed, 7 skipped (server-dependent tests), 0 failed.npx eslint public/js/setup.js tests/test-setup-preset-manual-reset.js scripts/run-tests.js: clean.npx prettier --check public/js/setup.jsstill fails — this file predates the repo's Prettier/ESLint CI gate (added in918801b, after this file's last edit) and has never been reformatted. A full reformat would touch ~4000 unrelated lines, so it's intentionally left out of this focused bugfix (confirmed with the repo maintainer). A separate repo-wide formatting pass would be a better fit for that.Closes #235
Generated by Claude Code