Skip to content

fix: reset hidden aiProvider when switching to manual AI configuration - #238

Open
admonstrator wants to merge 1 commit into
mainfrom
fix-235-setup-preset-aiprovider-reset
Open

fix: reset hidden aiProvider when switching to manual AI configuration#238
admonstrator wants to merge 1 commit into
mainfrom
fix-235-setup-preset-aiprovider-reset

Conversation

@admonstrator

Copy link
Copy Markdown
Owner

Summary

  • Selecting a named AI provider preset (e.g. "OpenAI (ChatGPT)") in the Step 5 setup wizard sets the hidden aiProvider input to that preset's provider value.
  • 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 and returning a misleading 401 that references OpenAI's own docs.
  • Fix: applyPreset()'s if (!preset) branch (manual mode) now resets aiProvider.value to 'custom' before updating the hint text.
  • Also fixed three small pre-existing ESLint issues in the same file (missing /* global Swal */ directive, an unused catch (_error) binding, a stale eslint-disable-next-line no-await-in-loop comment) so the full file passes this repo's changed-files ESLint gate.

Test plan

  • Added tests/test-setup-preset-manual-reset.js: selects a named preset, then applies null (manual mode), asserts aiProvider resets to custom. Registered in scripts/run-tests.js (quickstart area).
  • 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.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'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

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
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setup wizard: switching to "Manual custom configuration" doesn't reset the hidden aiProvider field → AI connection test hits the wrong endpoint

2 participants