Skip to content

Release v2026.10.01 - #381

Closed
admonstrator wants to merge 27 commits into
mainfrom
claude/release-review-2026-10-01-iax78p
Closed

admonstrator wants to merge 27 commits into
mainfrom
claude/release-review-2026-10-01-iax78p

Conversation

@admonstrator

@admonstrator admonstrator commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Before: main is at v2026.09.03 and does not have the work collected on nightly.

After: main carries nightly as v2026.10.01: Anthropic (Claude) as AI and OCR provider with Claude Haiku 5.5 as default, the Claude model picker as a dropdown, settings saves no longer sign everybody out (expired sessions go to the login page), the manual tagging page on the current design, and the lockfile security updates.

How: this branch is nightly with main merged in. The only conflict was the PAPERLESS_AI_VERSION line in config/config.js, which main's automated bump set to v2026.09.03; it is resolved to v2026.10.01 to match the newest changelog block. No October tag exists yet, so the release workflow will also generate v2026.10.01.

Two fixes found during the release review are added on top:

  • 417581b: the Claude API and Claude OCR base URLs strip trailing slashes with the existing linear helper instead of /\/+$/ (CodeQL polynomial regex alert).
  • c4a02a7: the settings AI/OCR test and model endpoints no longer substitute the stored Claude key when the request names a host that is not configured, so a caller cannot have the saved key sent to their own server. Same rule Quickstart already follows; covered by a new test.

Checks run on this branch: offline suite 117 passed, 0 failed; the server-dependent auth, rate-limit and thumbnail tests pass against a locally started server. ESLint, Prettier and stylelint clean on changed files, no OpenAPI drift. npm audit --omit=dev reports 0 vulnerabilities; the 9 remaining high findings are braces 3.0.3 in dev tooling only (nodemon, stylelint), with no fix available.

Included: #375, #378, #379, #380.

🤖 Generated with Claude Code

https://claude.ai/code/session_012u2T47w6ynKbZ2rxoc4Gf4

claude and others added 25 commits October 10, 2026 19:33
Background:
The Claude API is not OpenAI-compatible: the system prompt is a field of
its own, max_tokens is required, answers are typed content blocks, usage
is reported as input/output tokens, models can decline with
stop_reason "refusal", and the current models reject sampling parameters
and differ in whether thinking can be switched off. Pointing the custom
provider at it is therefore not enough; it needs a provider of its own.

Changes:
- Add @anthropic-ai/sdk.
- config.anthropic block: ANTHROPIC_API_KEY, ANTHROPIC_API_URL (proxy
  only), ANTHROPIC_MODEL (default claude-haiku-5-5), ANTHROPIC_EFFORT,
  ANTHROPIC_MAX_TOKENS (default 16000), ANTHROPIC_STRUCTURED_OUTPUT and
  ANTHROPIC_PROMPT_CACHING (both default yes).
- services/anthropicSupport.js: per-model request rules
  (anthropicModelTraits), thinking/effort fields (anthropicThinkingParams),
  completion cap, client factory, answer text, usage mapping and the
  truncation/refusal check shared by the AI and OCR paths.
- services/anthropicService.js: provider skeleton wired into
  AIServiceFactory as 'anthropic'.
- ANTHROPIC_API_KEY joins the bootstrap, settings and log redaction
  secret lists.
- Register test-anthropic-support plus three placeholder tests in a new
  'anthropic' test area.

Testing:
- node scripts/run-tests.js --all: 115 passed, 6 skipped (server-bound).
- test-anthropic-support covers the per-model thinking fields, effort
  filtering, completion cap, text/usage mapping and stop reasons.

Impact:
No behaviour change for the existing providers. AI_PROVIDER=anthropic
selects the skeleton, which does not analyse yet.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
The setup wizard validates an OCR provider by running a real OCR request
before the settings are saved, so the validation and the OCR service need
one function that takes the connection details as arguments.

Changes:
- mistralOcrService.runAnthropicOcr({ apiKey, apiUrl, model, base64,
  mimeType, timeoutMs }) placeholder; implemented in a follow-up commit.

Testing:
- eslint, prettier, test-mistral-ocr-image-payload.

Impact:
None; the method is not called yet.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
OCR_PROVIDER=anthropic was reserved by the contract commit, but
runAnthropicOcr() only threw. The OCR settings are shared with the other
providers in ways that would send the wrong things to the Claude API:
config.mistralOcr.apiKey falls back to MISTRAL_API_KEY, MISTRAL_OCR_MODEL
defaults to mistral-ocr-latest, and the Messages API has limits (32 MB per
request, 10 MB per image, PDF and four image types only) that otherwise
surface as a bare 413 or 400.

Changes:
- provider getter returns 'anthropic' for OCR_PROVIDER=anthropic;
  ollama/custom and everything else map as before. New providerLabel
  getter ("Local OCR" / "Claude OCR" / "Mistral OCR") replaces the inline
  ternary in processQueueItem().
- Claude OCR key: OCR_API_KEY, else ANTHROPIC_API_KEY, never the
  MISTRAL_API_KEY fallback. Model: MISTRAL_OCR_MODEL only when it names a
  Claude model (also with an anthropic./anthropic/ proxy prefix), else
  ANTHROPIC_MODEL. URL: OCR_API_URL when set, else ANTHROPIC_API_URL.
- runAnthropicOcr({ apiKey, apiUrl, model, base64, mimeType, timeoutMs,
  maxTokens }) sends one streamed Messages request: a document block for a
  PDF or an image block for PNG/JPEG/GIF/WebP, then the transcription
  instruction (reading order, Markdown tables, no translation, summary or
  commentary, [illegible] for unreadable spots, "[no text]" for an empty
  document, which is returned as ''). Thinking as low as the model allows
  via anthropicThinkingParams(model, { reasoning: false }); no sampling
  parameters; max_tokens 64000 (fits every current model and Claude Haiku
  4.5), raised by ANTHROPIC_MAX_TOKENS up to 128000. Answer read with
  anthropicMessageText(), stop reason checked with
  assertAnthropicCompleted(), usage logged via mapAnthropicUsage(). It
  reads no config, so the setup validation can test unsaved details; a
  data URL is accepted as input.
- Limits are checked before sending, with the limit in the message and an
  error code: claude_ocr_unsupported_type, claude_ocr_too_large,
  claude_ocr_empty_input.
- A PDF over 32 MB falls back to the poppler page rendering local OCR
  uses (same OCR_PDF_RENDER_* settings), one image request per page;
  without rendering or pdftoppm it fails naming the limit and the remedy.
  _performOcrOnRenderedPages() takes an optional page engine and label for
  that; local OCR keeps its defaults.
- A watchdog aborts a request after timeoutMs without progress (before the
  answer starts or between two streamed events) and reports the usual OCR
  timeout message; a long transcript that keeps arriving is not cut short.
  API errors become one line: "Claude API answered HTTP <status> (<type>):
  <message>".

Testing:
- npx eslint / npx prettier --check on the changed file.
- node scripts/run-tests.js --all: 115 passed, 6 skipped (server-bound),
  same as before; existing OCR tests unchanged and green.
- node scripts/regen-openapi.js: no spec change.

Impact:
A truncated or refused transcript throws, so processQueueItem() fails the
item exactly like a truncated local OCR result (queue 'failed', failure
recorded as ocr_failed, nothing written to Paperless-ngx). Mistral and local
OCR behave as before.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
tests/test-ocr-provider-anthropic.js was a placeholder registered by the
contract commit (area 'anthropic').

Changes:
- A real HTTP server on an ephemeral port answers like the Messages API,
  including streamed answers (thinking and text blocks, stop reasons,
  stop_details, error bodies, delayed headers, stalled and slow streams).
  The real SDK talks to it; the configuration is the real config module
  loaded from environment variables in an empty working directory.
- 19 cases: provider selection and labels; key precedence (OCR_API_KEY,
  then ANTHROPIC_API_KEY, never MISTRAL_API_KEY, no key means no request);
  model selection (Mistral default replaced, Claude model kept) and URL
  precedence; PDF and PNG request shape (block type and order, media type,
  stream, max_tokens 64000, no temperature/top_p/top_k, thinking off per
  model for Haiku 5.5, Sonnet 5.5 and Opus 5.5); text read past a leading
  thinking block and the "[no text]" marker; max_tokens and refusal fail
  the queue item without write-back; a completed transcript is written
  back; TIFF, office types, an image over 10 MB and a PDF over 32 MB
  without poppler fail unsent with the limit named; a PDF over 32 MB with
  poppler goes out as page images; runAnthropicOcr uses its arguments, not
  config; API errors as one line; stall, silent server and slow-but-alive
  stream; ANTHROPIC_MAX_TOKENS raising the OCR cap up to 128000.

Testing:
- node tests/test-ocr-provider-anthropic.js: 19 passed (about 4 s).
- Mutation check: replacing the key, model, thinking, completion check,
  no-text marker, watchdog refresh or provider routing each fails at least
  one case.
- node scripts/run-tests.js --all: 115 passed, 6 skipped (server-bound).

Impact:
Test-only.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
Claude reads PDFs and images natively, so it can serve as an OCR
provider next to Mistral and local vision models.

Changes:
- OCR_PROVIDER=anthropic routes performOcr to runAnthropicOcr: one
  streamed Messages request with the PDF as a document block or the
  image as an image block, thinking off, no temperature.
- Key: OCR_API_KEY, else ANTHROPIC_API_KEY, never MISTRAL_API_KEY.
  Model: MISTRAL_OCR_MODEL only when it names a Claude model, else
  ANTHROPIC_MODEL. URL: OCR_API_URL, else ANTHROPIC_API_URL.
- Limits checked before sending (32 MB request, 10 MB image, PDF and
  png/jpeg/gif/webp only); a PDF over the request limit falls back to
  poppler page rendering. Truncation and refusal fail the queue item
  without writing partial text back.

Testing:
- test-ocr-provider-anthropic: 19 cases against a fake Messages API.
- node scripts/run-tests.js --all: 115 passed, 6 skipped.

Impact:
New OCR provider; existing providers unchanged.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
calculateTokens() and truncateToTokenLimit() run tiktoken for OpenAI
model names and a four-characters-a-token estimate for everything else.
The current Claude tokenizers count noticeably more tokens per character
than that (the newest generation about 30% more than the one before), so
the estimate would come out low and let through documents the token
budget meant to cut. The Duplicates review also names the model it
measures its prompts against per provider and would have reported the
OpenAI model for the Anthropic provider.

Changes:
- serviceUtils: models named claude-* get their own estimate of three
  characters a token in calculateTokens() and truncateToTokenLimit(),
  never tiktoken; the truncation breaks at a word boundary like the
  other estimated models. Every other model is unchanged.
- entityMatchAiService.modelName(): case 'anthropic' returns
  ANTHROPIC_MODEL.

Testing:
- Covered by tests/test-anthropic-provider.js in the provider commit
  (Claude estimate, unchanged estimate for other models, modelName).
- eslint and prettier on both files.

Impact:
Only requests for claude-* models estimate differently. No behaviour
change for the existing providers.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
Every provider service assembles the document-analysis prompt itself;
the four copies agree only because they were copied from each other. The
Anthropic provider needs the same prompt and, for structured output, a
JSON schema of the object that prompt asks for. A fifth copy would drift.

Changes:
- services/analysisPrompt.js, free of I/O:
  - buildAnalysisPrompt(): customService's assembly byte for byte
    (custom-fields template, USE_EXISTING_DATA block, restriction
    placeholders, external API data, USE_PROMPT_TAGS, webhook prompt),
    returning the system prompt, the prompt tags it counts, the template,
    the parsed custom fields, a parse error for the caller to log and
    which response shape the prompt asks for. A CUSTOM_FIELDS value
    without a custom_fields list falls back to no fields instead of the
    TypeError customService runs into.
  - analysisResponseSchema(): the JSON schema of that object, every
    object closed with additionalProperties: false, every key required,
    correspondent/document_type/document_date and custom field values
    nullable, custom field names fixed to the configured names; the
    predefined-tags shape without document type and custom fields.
  - parseAnalysisAnswer(): customService's tolerant JSON parser.
- Only the Anthropic provider uses it in this change; the other four
  providers are untouched and can switch later.

Testing:
- Covered by tests/test-anthropic-provider.js in the provider commit,
  including a byte-for-byte comparison with the system prompt
  customService sends in nine configurations.
- eslint and prettier.

Impact:
None on its own; the module is not used by an existing provider.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
AI_PROVIDER=anthropic selected a skeleton that could not analyse. The
Claude Messages API differs from the OpenAI dialect in request shape,
streaming events, usage, stop reasons and per-model thinking rules, so it
needs its own provider service with the same contract as the other four.

Changes:
- anthropicService:
  - analyzeDocument(): thumbnail caching, external API data, the prompt
    from buildAnalysisPrompt(), token budget and truncation, response log
    and validation as in customService, sent as a Messages request with a
    top-level system field, one user message, max_tokens from
    ANTHROPIC_MAX_TOKENS, thinking/effort from anthropicThinkingParams()
    and no sampling parameters. Answers are read with
    anthropicMessageText(), checked with assertAnthropicCompleted()
    (ai_response_truncated / ai_refused, neither routed to OCR) and
    metrics come from mapAnthropicUsage(). A missing thumbnail no longer
    aborts the analysis.
  - Structured output (ANTHROPIC_STRUCTURED_OUTPUT): output_config.format
    with analysisResponseSchema() where the model supports it, merged
    with the effort. A schema the API refuses with 400 is retried once
    without it and then skipped for the rest of the process.
  - Prompt caching (ANTHROPIC_PROMPT_CACHING): the system prompt goes out
    as one text block with cache_control, for the analysis and for
    generateText with a system prompt; one debug line per request with
    the cache reads and writes.
  - generateText(): systemPrompt, model, maxTokens, signal, reasoning and
    onProgress as documented in aiGenerateOptions; temperature accepted
    and ignored; lastGenerateTextUsage set from the final message.
  - checkStatus(): models.retrieve(model), no tokens spent; key and model
    errors told apart by SDK error class.
  - Every request is streamed: the SDK refuses non-streamed requests whose
    max_tokens could take longer than ten minutes.
- aiGenerateOptions: runMessagesStream()/consumeMessagesStream() feed the
  existing progress collector from Messages events (text deltas as
  answer, thinking deltas as reasoning), take stop reason and usage from
  the final message and report done exactly once on success, truncation
  and abort.
- tests/helpers/fake-anthropic.js: real HTTP server for POST /v1/messages
  (JSON and SSE) and GET /v1/models/:id.

Testing:
- tests/test-anthropic-provider.js, 25 cases against the fake through the
  real SDK: request shape, thinking/effort for Haiku 5.5, Sonnet 5.5 and
  Opus 5.5, schema on/off/unsupported, cache_control on/off, identical
  system prompt across documents, thinking-first answers, usage and
  cache log, truncation, refusal (not queued for OCR), schema refusal
  fallback, predefined-tags schema, custom-fields schema, byte-for-byte
  prompt parity with customService, generateText plain/streamed/usage/
  options/truncation/refusal, abort mid-stream and before the request,
  checkStatus, token estimate, Duplicates model name, timeout, no key.
- node scripts/run-tests.js --all: 115 passed, 6 skipped (server-bound).
- eslint and prettier on every changed file; OpenAPI regeneration
  produces no diff.

Impact:
AI_PROVIDER=anthropic now analyses documents, generates text for the
Duplicates review and tag simplification, and reports its status. No
behaviour change for the existing providers.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
The Claude API needs a provider of its own; the contract added the
configuration and shared request rules, this fills in the service.

Changes:
- anthropicService: analyzeDocument, generateText (streamed progress,
  abort, truncation and refusal like the other providers) and a
  token-free checkStatus via models.retrieve. Every request is
  streamed because the SDK refuses non-streamed requests with large
  max_tokens.
- Stage 2: structured output. The analysis request carries a JSON
  schema derived from the prompt and the configured custom fields; a
  schema the API rejects is retried once without and remembered per
  model.
- Stage 3: prompt caching. The system prompt goes out as a cacheable
  block for analysis and generateText.
- analysisPrompt.js: pure prompt builder taken from customService, the
  response schema and the tolerant answer parser; only the Anthropic
  provider uses it so far.
- aiGenerateOptions: runMessagesStream feeding the shared progress
  collector. serviceUtils: about 3 characters per token for claude-*.
  entityMatchAiService: model name for anthropic.

Testing:
- test-anthropic-provider: 25 cases against a fake Messages API,
  including a byte-for-byte system prompt comparison with
  customService in nine configurations.
- node scripts/run-tests.js --all: 115 passed, 6 skipped.

Impact:
AI_PROVIDER=anthropic analyses documents. Other providers unchanged.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
CLAUDE.md listed four AI providers and three OCR modes.

Changes:
- Name anthropic in the provider list and OCR modes, point at
  anthropicSupport.js and analysisPrompt.js, and note the Claude token
  estimate.

Testing:
- prettier --check CLAUDE.md.

Impact:
Documentation only.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
Anthropic becomes a fifth AI provider and a fourth OCR provider. The setup
wizard and the settings page prove a configuration before it is saved, list
the models a provider offers, and decide at startup whether the stored
configuration is complete. None of that knew the provider.

Changes:
- validateAnthropicConfig(apiKey, model, apiUrl) proves key and model with
  client.models.retrieve(model) (GET /v1/models/{id}), so a test spends no
  tokens; a proxy URL goes through validateApiUrl with the setup options the
  custom provider uses. Wired into validateConfig and
  hasRequiredConfiguration (key + model).
- discoverAnthropicModels({ apiKey, apiUrl }) returns { models, live, error }:
  the live list from client.models.list(), or ANTHROPIC_SUGGESTED_MODELS when
  there is no key, the URL is refused or the call fails.
- Claude OCR: resolveAnthropicOcrConnection() (OCR key, then the Anthropic AI
  key; OCR URL, then the AI URL), resolveAnthropicOcrModel() (a claude-* OCR
  model, else the AI model, else the default) and validateAnthropicOcrConfig(),
  a real round trip through mistralOcrService.runAnthropicOcr() on the
  existing validation image. validateOcrConfig, detectOcrApiUrlForSetup and
  discoverOcrModels accept 'anthropic'; detectAiApiUrlForSetup and
  discoverAiModels accept it for the AI side.
- The runtime configuration snapshot lists ANTHROPIC_API_KEY, ANTHROPIC_MODEL
  (defaulting to ANTHROPIC_DEFAULT_MODEL, so an injected key alone is a
  complete configuration) and ANTHROPIC_API_URL.

Testing:
- tests/test-anthropic-settings.js (filled in a later commit of this series)
  runs the validation and discovery against a fake /v1/models server.
- npx eslint, npx prettier --check.

Impact:
Additive; existing providers take the same paths as before.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
The routes behind the setup wizard and the settings page hard-code the
provider lists, the keys each provider stores and the OCR key handling. For
Claude OCR the shared OCR fields are a trap: OCR_API_KEY and OCR_API_URL are
read before the Anthropic AI settings, so a Mistral key or an auto-detected
Ollama URL left there would end in a 401 or a request to the wrong host.

Changes:
- "Anthropic (Claude)" preset (model ANTHROPIC_DEFAULT_MODEL, no URL, token
  placeholder sk-ant-...) in DEFAULT_AI_PROVIDER_PRESETS and in
  config/ai-provider-presets.json, which replaces the defaults when present.
- getAnthropicFormOptions() renders as `anthropicOptions` on /setup and
  /settings: default model, suggested models, effort levels, default max
  tokens.
- Every provider list (setup complete, AI test, presets filter, settings save)
  accepts 'anthropic'; every OCR provider list accepts 'anthropic'.
- /api/setup/complete writes ANTHROPIC_API_KEY, ANTHROPIC_MODEL and, only when
  set, ANTHROPIC_API_URL; for Claude OCR it never writes the key into
  MISTRAL_API_KEY and refuses when no Anthropic key can be found.
- POST /settings: anthropicApiKey/Model/Effort/MaxTokens/StructuredOutput/
  PromptCaching/ApiUrl map to the ANTHROPIC_* keys (saved while anthropic is
  selected; an empty key keeps the stored one). Effort must be empty or one
  of ANTHROPIC_EFFORT_LEVELS, max tokens a positive whole number, the URL
  http(s); anything else is a 400.
- Claude OCR on save: when the OCR provider crosses to or from anthropic, a
  stored OCR_API_KEY is cleared unless a new key is typed, and a typed key is
  not mirrored into MISTRAL_API_KEY. A leftover OCR_API_URL is cleared unless
  the operator typed a different (proxy) URL. The OCR model is stored as the
  Claude model it will run with.
- resolveOcrApiKeyForProvider(): the OCR test/model routes never hand
  MISTRAL_API_KEY, or an OCR_API_KEY saved for another provider, to Claude.
  tests/test-ocr-model-fetch-key.js counts the three call sites of the new
  resolver instead of the old one.
- Model discovery for anthropic (AI and OCR) returns the live list or the
  suggested models with `live: false` and a message that says which.
- /manual/analyze routes anthropic through AIServiceFactory and records its
  token metrics; /about support info gains anthropicModel, anthropicApiUrl
  and anthropicKeySet.
- ENV export, env preview and the settings config maps list the ANTHROPIC_*
  keys; @Swagger docs for /api/settings/ai/test, /api/settings/ocr/test (new
  block), /api/settings/ocr/models and POST /settings list anthropic and the
  new fields; OPENAPI/openapi.json regenerated.

Testing:
- node scripts/run-tests.js --all; test-anthropic-settings covers the save,
  the refusals, the stale-key and stale-URL rules and setup completion.
- node scripts/regen-openapi.js; npx eslint; npx prettier --check.

Impact:
Additive for existing providers. Switching the OCR provider to or from Claude
without typing a key now clears the stored OCR key, so it cannot reach the
wrong vendor.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
The bug report prefill and the About diagnostics list every provider's
non-secret settings and whether its key is set; Anthropic was missing. The
Claude provider can also fail a document with the new reason code
ai_refused, which the failed-documents page showed as a raw code.

Changes:
- server.js res.locals appAnthropicModel, appAnthropicApiUrl ('default' when
  empty) and appAnthropicKeySet; the sidebar bug-report payload reads them
  behind typeof guards, so views rendered without them keep working.
- public/js/bug-report-link.js adds ANTHROPIC_MODEL, ANTHROPIC_API_URL and
  ANTHROPIC_API_KEY=<set|missing> to config_notes; the About diagnostics add
  the same three lines.
- public/js/failed.js labels ai_refused "Declined by the AI model".

Testing:
- node scripts/run-tests.js --all (the shared-partial UI tests render the
  sidebar without the new locals).
- npx eslint, npx prettier --check.

Impact:
Additive; no secret value is exposed, only whether the key is set.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
…tup pages

Background:
With the routes in place, the settings page and the setup wizard still could
not select Anthropic or Claude OCR, and their page scripts assumed every
provider other than OpenAI and Azure needs an API URL.

Changes:
- Settings page: AI provider option "Anthropic (Claude)" and an
  #anthropicSettings block from existing zr components: API key (password,
  "Configured" placeholder, empty keeps the stored value), Claude model (text
  input with the #anthropicModelOptions datalist and #fetchAnthropicModelsBtn
  for the live list), effort select ("Model default" plus every level),
  max tokens, structured output and prompt caching switches, optional API URL
  ("Only for a proxy; leave empty for api.anthropic.com"). Both temperature
  fields get a [data-anthropic-hint] note that Claude models ignore them.
- OCR provider option "Claude (Anthropic)" on both pages: the key is
  optional ("Empty uses the Anthropic API key from the AI provider"), the URL
  field is an optional proxy URL and is emptied when switching to Claude, the
  model select offers the suggested Claude models with the AI model
  preselected, and a Claude model is reset when switching back to Mistral.
  PDF rendering options stay hidden, as for Mistral (Claude reads PDFs).
- Setup wizard: the Anthropic preset fills the model select with the
  suggested models; the API URL is optional for anthropic (#aiApiUrlHint) and
  the token required; the env preview shows AI_PROVIDER=anthropic,
  ANTHROPIC_API_KEY, ANTHROPIC_MODEL (and ANTHROPIC_API_URL when set) and
  OCR_PROVIDER=anthropic.
- OCR test, model discovery and save payloads send the Anthropic AI key, URL
  and model while Anthropic is the AI provider, and no Mistral default model
  for Claude OCR. The "reset local overrides" mapping covers the new fields.
- tests/test-anthropic-settings.js: hasRequiredConfiguration, validation and
  validateConfig routing against a fake /v1/models server, the discovery
  fallback, validateOcrConfig for anthropic with runAnthropicOcr stubbed (key
  fallback, empty transcript, SSRF), the bootstrap sanitizer, the settings and
  setup renders, the settings save and its refusals, the stale OCR key and URL
  rules, and setup completion.

Testing:
- node tests/test-anthropic-settings.js (24 cases);
  node scripts/run-tests.js --all.
- Rendered /settings and /setup through the real router on a throwaway server
  and looked at screenshots, desktop and 390 px, light and dark, with
  Anthropic selected for AI and OCR; switching OCR from custom to Claude
  emptied the URL and preselected the AI model.
- npx eslint, npx prettier --check.

Impact:
Additive; the pages behave as before for every other provider.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
The provider and Claude OCR need to be selectable and configurable
without editing environment variables by hand.

Changes:
- Setup wizard: "Anthropic (Claude)" preset (claude-haiku-5-5, token,
  no URL), env preview, and Claude as OCR provider.
- Settings: Anthropic block (API key, model with live list and
  suggestions, effort, max tokens, structured output, prompt caching,
  optional proxy URL), temperature hints, and Claude OCR with an
  optional key that falls back to the Anthropic key.
- setupService: token-free key/model validation via models.retrieve,
  model discovery with a fallback list, and a real Claude OCR round
  trip on the validation image.
- Routes accept anthropic everywhere providers are listed; manual
  analysis goes through the factory; OCR keys are never mirrored into
  MISTRAL_API_KEY for Claude and a stale OCR_API_URL is cleared.
- Support info, failed reasons ("Declined by the AI model") and the
  OpenAPI spec cover the new provider.

Testing:
- test-anthropic-settings: 24 cases.
- Settings and wizard reviewed in a mock server, desktop and 390 px,
  light and dark.
- node scripts/run-tests.js --all: 115 passed, 6 skipped.

Impact:
Anthropic can be configured from the UI.

Upstream Status:
Not applicable (fork-only feature).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
feat: Anthropic (Claude) as AI and OCR provider
Background:
JWT_SECRET is minted once at first start and appears in no form, while
setupService.saveConfig() rewrites runtime-overrides.json (and data/.env
in legacy mode) whole. The settings page saves without the secret, so
the restart that follows every save found none, minted a new one and
invalidated every session, including the one that had just saved.

The page did not notice. Signed-in routes answer an invalid session with
a redirect to /login, fetch follows it, and callers received the login
page as a 200 they could not parse. Fetching OCR models after a save,
for example, reported "Model discovery failed" with nothing in the log.

Changes:
- setupService.saveConfig() adds the running JWT_SECRET when the
  configuration it is given does not carry one. A secret injected
  through the environment stays protected and is never written.
- public/js/session-guard.js, loaded after csrf.js on every page, sends
  the browser to /login when a request ends there, once, and never on
  the login page itself.

Testing:
- tests/test-settings-save-session-secret.js: the secret survives a
  save and a fresh process start in runtime-first and legacy mode; a
  carried secret wins; an injected one is not persisted. Fails without
  the fix.
- tests/test-session-guard.js runs csrf.js and session-guard.js together,
  including the stale-token retry that ends at /login. Fails without
  the guard.
- node scripts/run-tests.js --all: 117 passed, 6 skipped. ESLint,
  Prettier, stylelint clean; OpenAPI unchanged.

Impact:
Saving settings no longer signs anybody out. A session that does end
(expiry, a reset of local overrides) now leads to the login page instead
of generic errors.

Upstream Status:
Not reported upstream; the settings save path is fork-specific.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background:
The manual tagging page still carried pieces from before the move off
Tailwind. Its tag chips were built with Tailwind utility classes that
no longer exist, so tags rendered as bare text next to unstyled icon
buttons. The modules had no head/body structure, so the analyze button
and the preview boxes ran edge to edge, and the preview lost the
document's line breaks and stretched the page to the document's length.

Changes:
- views/manual.ejs: every card uses .zr-module__head and __body; the
  correspondent and title are .zr-kv rows, the preview a .zr-panel; Save
  Tags sits in the Tags card head; section labels are .zr-label.
- manual-scripts.ejs: tags are .zr-chip elements with typed, labelled
  .zr-link buttons, the markup settings.ejs and the styleguide use; AI
  suggestions carry an outlined modifier.
- pages/shared.css: the unused .stat-box rules are gone; the preview
  keeps line breaks and scrolls within 600px (320px on phones); empty
  tag lists show a short hint.
- badges.css: chip icons are sized to their 15px buttons instead of
  overflowing past the chip's rounded end, on every page with chips.

Testing:
- Rendered with mock data in a throwaway server, desktop and 390px,
  light and dark, empty and with a document and AI suggestions loaded.
- Adding a suggestion, removing a tag and adding one from the select
  still update the hidden tags field (checked in the browser).
- node scripts/run-tests.js --all: 117 passed, 6 skipped. Prettier and
  stylelint clean.

Impact:
The manual page matches the rest of the interface. Element IDs and the
page's requests are unchanged.

Upstream Status:
Not applicable; the zr interface is fork-specific.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
Background
The next release carries the Anthropic provider (merged into nightly
earlier) and the two fixes in this branch. The What's New modal and the
Settings changelog only show what config/changelog.js lists, and its
newest block has to match PAPERLESS_AI_VERSION.

Changes
- config/changelog.js: new release block v2026.10.01 with four entries:
  Claude as AI provider, Claude as OCR provider, settings save no longer
  ending sessions, manual tagging page back on the current design.
- config/config.js: PAPERLESS_AI_VERSION bumped to v2026.10.01. The
  version follows the release workflow's scheme (current month, first
  build); the latest tag is v2026.09.03 and October has none yet.

Testing
- node scripts/run-tests.js --all: 117 passed, 6 skipped (need a running
  server), 0 failed; changelog-releases checks the version sync.
- Prettier and ESLint clean on both files; OpenAPI regeneration leaves
  the spec unchanged.

Impact
After the update the What's New modal shows the four entries. No
behaviour change.

Upstream Status
Fork-only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
fix: keep sessions across a settings save; repair the manual tagging page
Background
npm audit on nightly reported 14 advisories (1 critical, 11 high, 2
moderate). Two of them ship in the production image: proxy-addr under
express (IP spoofing via IPv4-mapped IPv6 trust subnets, critical) and
ip-address under express-rate-limit (SSRF and subnet-check bypasses).

Changes
- package-lock.json only, via npm audit fix without --force:
  proxy-addr 2.0.7 -> 2.0.8, ip-address 10.4.0 -> 10.7.4,
  brace-expansion 5.0.9 -> 5.0.12, source-map-js 1.2.1 -> 1.2.2,
  postcss-selector-parser 7.1.5 -> 7.1.6, stylelint 17.15.0 -> 17.16.0.
- No range in package.json changes.

Testing
- node scripts/run-tests.js --all: 117 passed, 6 skipped (need a running
  server), 0 failed.
- npm run lint:css clean with the newer stylelint.
- npm audit: 9 high left, all from braces 3.0.3 (GHSA-vfj7-8cjw-p6xm),
  for which no fixed version is published. It is reached only through
  nodemon and stylelint, both devDependencies that the image does not
  install (npm ci --omit=dev).

Impact
The production image no longer carries the critical proxy-addr advisory
or the ip-address advisories. No behaviour change intended.

Upstream Status
Fork-only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
fix(deps): close the fixable npm advisories on nightly
… model

Background
Every model field in Settings is a select filled by its Fetch button:
OpenAI, Ollama, custom and the OCR model. The Claude model was a text
input with a datalist instead. It behaved differently from the rest:
the suggestions only appeared while the field was empty or matched, so
a configured model hid all others, and a fetched list was easy to miss.

Changes
- views/settings.ejs: #anthropicModel is a select. It lists the
  suggested Claude models plus the configured one when that is not among
  them, with the configured (or default) model selected. The datalist is
  gone.
- public/js/settings.js: Fetch Models fills the select through
  populateModelSelect, the helper the other model fields use, keeping the
  current choice selected and listed even when the answer omits it. A
  preset selection goes through the same helper, so a preset model that
  is not listed yet is added instead of leaving the select empty.
- tests/test-anthropic-settings.js: the render assertions check the
  select and its selected option instead of the datalist.

Testing
- node scripts/run-tests.js --all: 117 passed, 6 skipped (need a running
  server), 0 failed.
- Settings rendered through the router test harness in Chromium against
  a fake Claude API, light and dark: the select shows the configured
  model, Fetch Models lists the five live models and keeps the selection,
  no page errors.
- Prettier and ESLint clean on the changed files.

Impact
The Claude model is chosen the same way as every other model. Typing a
model id that is neither suggested nor returned by the API is no longer
possible in the form; ANTHROPIC_MODEL still accepts any id.

Upstream Status
Fork-only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dZrUuuXyUkHuSP5sYdnML
fix(settings): pick the Claude model from a dropdown like every other model
…-2026-10-01-iax78p

# Conflicts:
#	config/config.js
Comment thread services/anthropicSupport.js Fixed
…ear time

Background:
CodeQL flagged /\/+$/ on the Claude API URL as a polynomial regular
expression: on a value made of many slashes it backtracks quadratically.
The same pattern was added for the Claude OCR base URL.

Changes:
- createAnthropicClient() and MistralOcrService.apiBase (anthropic branch)
  use the existing stripTrailingSlashes() helper from serviceUtils, which
  scans once from the end.

Testing:
- node scripts/run-tests.js --all: 117 passed, 0 failed
- ESLint and Prettier clean on both files

Impact:
Same result for every URL; no quadratic cost on crafted input.

Upstream Status:
Not applicable (fork-only provider).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u2T47w6ynKbZ2rxoc4Gf4
Background:
The settings AI and OCR test/model endpoints treat an empty key field as
"use the stored key". For Claude they did so whatever URL the request
named, so a signed-in caller could send apiUrl=https://some.host with an
empty key and receive ANTHROPIC_API_KEY (or the Claude OCR_API_KEY) in the
x-api-key header. The same file already refuses this for Quickstart
(resolveSettingsQuickstartApiKey); Claude did not follow that rule.

Changes:
- anthropicSupport: isConfiguredAnthropicHost() accepts an empty URL,
  ANTHROPIC_API_URL (empty = api.anthropic.com) and, while Claude is the
  OCR provider, OCR_API_URL.
- routes/setup.js: resolveSettingsAiToken() and
  resolveOcrApiKeyForProvider() withhold the stored Claude key from any
  other host. Typed keys are unchanged.
- setupService.resolveAnthropicOcrConnection(): the ANTHROPIC_API_KEY
  fallback follows the same rule, which also covers the setup routes.
- Tests: the stored-key case now configures its host; a new case checks
  that AI models, OCR models and the OCR connection get no stored key for
  an unconfigured host (fails without the fix).

Testing:
- node scripts/run-tests.js --all: 117 passed, 0 failed
- ESLint and Prettier clean on changed files, no OpenAPI drift

Impact:
No change for the settings page, which sends the configured URL. Testing
a new proxy URL now needs the key typed in, as with Quickstart.

Upstream Status:
Not applicable (fork-only provider).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u2T47w6ynKbZ2rxoc4Gf4
@admonstrator
admonstrator marked this pull request as ready for review October 12, 2026 19:12
@admonstrator admonstrator mentioned this pull request Oct 13, 2026
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.

3 participants