Repository navigation
Release v2026.10.01 - #381
Closed
admonstrator wants to merge 27 commits into
Closed
admonstrator wants to merge 27 commits into
admonstrator wants to merge 27 commits into
Conversation
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
…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
marked this pull request as ready for review
October 12, 2026 19:12
Merged
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.
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_VERSIONline inconfig/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:
/\/+$/(CodeQL polynomial regex alert).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=devreports 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