Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bdd0e03
feat(ai): contract for the Anthropic provider
claude Oct 10, 2026
f4edcff
feat(ocr): contract seam for Claude OCR
claude Oct 10, 2026
b30d395
feat(ocr): Claude as an OCR provider
claude Oct 10, 2026
c991fc5
test(ocr): Claude OCR against a fake Messages API
claude Oct 10, 2026
e52b6d8
Merge Claude OCR provider (stage 4)
claude Oct 10, 2026
e6370fc
feat(ai): token estimate for Claude models
claude Oct 10, 2026
00fd394
feat(ai): shared analysis prompt builder with response schema
claude Oct 10, 2026
a72e98d
feat(ai): Anthropic provider with structured output and prompt caching
claude Oct 10, 2026
33ff674
Merge Anthropic provider service (stages 1-3)
claude Oct 10, 2026
c853bca
docs(claude-md): describe the Anthropic provider
claude Oct 10, 2026
6ba9f74
feat(setup): validate and discover Anthropic models in setupService
claude Oct 10, 2026
4506a97
feat(settings): accept Anthropic in the setup and settings routes
claude Oct 10, 2026
f0e7a80
feat(support): report Anthropic in support info and failed reasons
claude Oct 10, 2026
0b50832
feat(settings): Anthropic block and Claude OCR on the settings and se…
claude Oct 10, 2026
8573ffd
Merge Anthropic setup, settings and OCR provider UI
claude Oct 10, 2026
fd98ded
Merge pull request #375 from admonstrator/claude/project-thread-ns6mxp
admonstrator Oct 12, 2026
0a67fa0
fix(auth): keep sessions across a settings save, and say when one ends
claude Oct 12, 2026
2df6fc9
fix(manual): rebuild the manual tagging page on the zr components
claude Oct 12, 2026
971e73c
chore(release): changelog and version for v2026.10.01
claude Oct 12, 2026
8b932da
Merge pull request #378 from admonstrator/claude/project-thread-ns6mxp
admonstrator Oct 12, 2026
a3301b4
fix(deps): close the fixable npm advisories on nightly
claude Oct 12, 2026
dccb236
Merge pull request #379 from admonstrator/claude/project-thread-ns6mxp
admonstrator Oct 12, 2026
8e2c30e
fix(settings): pick the Claude model from a dropdown like every other…
claude Oct 12, 2026
ee19ffe
Merge pull request #380 from admonstrator/claude/project-thread-ns6mxp
admonstrator Oct 12, 2026
4a84682
Merge remote-tracking branch 'origin/main' into claude/release-review…
claude Oct 12, 2026
417581b
fix(anthropic): strip trailing slashes from the Claude API URL in lin…
claude Oct 12, 2026
c4a02a7
fix(anthropic): only send the stored Claude key to a configured host
claude Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## What this is

Paperless-AI **next** — a community fork of clusterzx/paperless-ai. A Node.js/Express app that uses
LLMs (OpenAI, Ollama, Azure, or any OpenAI-compatible "custom" endpoint) plus optional OCR to auto-tag,
LLMs (OpenAI, Ollama, Azure, Anthropic, or any OpenAI-compatible "custom" endpoint) plus optional OCR to auto-tag,
title, classify, and extract metadata for documents in a Paperless-ngx instance. Server-rendered EJS UI,
no frontend framework. Single SQLite database (better-sqlite3, WAL mode).

Expand Down Expand Up @@ -54,12 +54,18 @@ the `services/` singletons.
`class Foo { ... }; module.exports = new Foo();`. Import and call directly; do not `new` them.

**AI provider abstraction:** `services/aiServiceFactory.js` returns the right service based on
`config.aiProvider` (`openai` | `ollama` | `custom` | `azure`). Every provider service implements
`config.aiProvider` (`openai` | `ollama` | `custom` | `azure` | `anthropic`). Every provider service implements
`analyzeDocument(content, doc, existingTags, correspondents)`. To add a provider: add a service singleton,
wire it into the factory switch, and add its config block in `config/config.js`.

**Token handling:** `services/serviceUtils.js` — `calculateTokens()` (tiktoken for OpenAI, ÷4 char
estimate otherwise) and `truncateToTokenLimit()`. Also holds AI-error → OCR-fallback classification
**Anthropic** is not OpenAI-compatible and has its own service (`services/anthropicService.js`). Per-model
request rules (thinking, effort, structured output, cache minimums), the client factory, usage mapping and
the truncation/refusal check live in `services/anthropicSupport.js` and are shared with Claude OCR; never
send `temperature` to it or read `content[0]`. Its analysis prompt and JSON schema come from
`services/analysisPrompt.js`, which the other providers do not use yet.

**Token handling:** `services/serviceUtils.js` — `calculateTokens()` (tiktoken for OpenAI, ÷3 char
estimate for `claude-*`, ÷4 otherwise) and `truncateToTokenLimit()`. Also holds AI-error → OCR-fallback classification
helpers used by the scan loop.

**Prompt restrictions:** `services/restrictionPromptService.js` replaces placeholders like
Expand All @@ -69,7 +75,7 @@ in prompts. The base prompts live in `config/config.js` (`specialPromptPreDefine
normalize them with `toNameList()` from `services/serviceUtils.js` instead of joining them directly.

**OCR:** `services/mistralOcrService.js` — provider can be `mistral`, `ollama` (native `/api/chat`
vision), or OpenAI-compatible `/v1`. OCR is a fallback path when AI analysis fails on low-text docs.
vision), OpenAI-compatible `/v1`, or `anthropic` (Claude reads PDFs and images natively). OCR is a fallback path when AI analysis fails on low-text docs.

**Database:** `models/document.js` — better-sqlite3 at `data/documents.db`, WAL mode, prepared
statements throughout. Key tables: `processed_documents`, `history_documents` (server-side paginated
Expand Down
183 changes: 176 additions & 7 deletions OPENAPI/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -4609,6 +4609,10 @@
"nullable": true,
"description": "Recommended default; absent for providers without model classification"
},
"live": {
"type": "boolean",
"description": "anthropic only — false when the live list failed and the suggested Claude models are returned instead"
},
"resolvedApiUrl": {
"type": "string"
},
Expand Down Expand Up @@ -4697,17 +4701,21 @@
"openai",
"ollama",
"custom",
"azure"
"azure",
"anthropic"
]
},
"apiUrl": {
"type": "string"
"type": "string",
"description": "Not needed for anthropic unless a proxy sits in front of api.anthropic.com"
},
"token": {
"type": "string"
"type": "string",
"description": "Omit or leave empty to use the stored key of the selected provider"
},
"model": {
"type": "string"
"type": "string",
"description": "For anthropic, checked with a models lookup that spends no tokens"
},
"setupValidationTimeoutMs": {
"type": "integer"
Expand All @@ -4727,6 +4735,86 @@
}
}
},
"/api/settings/ocr/test": {
"post": {
"summary": "Test OCR provider connectivity from the settings page",
"description": "Runs one OCR request on a generated test image. For `anthropic` the\nkey is the OCR key when one is given or stored for Claude OCR, and\notherwise the Anthropic AI provider's key (`anthropicApiKey`, then\nthe stored ANTHROPIC_API_KEY).\n",
"tags": [
"Settings"
],
"security": [
{
"BearerAuth": []
},
{
"ApiKeyAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"enabled": {
"type": "string",
"enum": [
"yes",
"no"
]
},
"provider": {
"type": "string",
"enum": [
"mistral",
"custom",
"ollama",
"anthropic"
]
},
"apiUrl": {
"type": "string",
"description": "Ignored for mistral. For anthropic an optional proxy URL; empty uses the Anthropic AI provider's URL, then api.anthropic.com"
},
"apiKey": {
"type": "string",
"description": "Omit or leave empty to use the stored key"
},
"model": {
"type": "string",
"description": "For anthropic, a Claude model; anything else falls back to the AI model"
},
"setupOcrValidationTimeoutMs": {
"type": "integer"
},
"anthropicApiKey": {
"type": "string",
"description": "Anthropic AI key typed but not saved yet; Claude OCR falls back to it"
},
"anthropicApiUrl": {
"type": "string",
"description": "Anthropic AI proxy URL typed but not saved yet"
},
"anthropicModel": {
"type": "string",
"description": "Anthropic AI model typed but not saved yet"
}
}
}
}
}
},
"responses": {
"200": {
"description": "OCR connectivity result returned"
},
"401": {
"description": "Unauthorized"
}
}
}
},
"/api/settings/ocr/models": {
"post": {
"summary": "Discover available OCR models from the settings page",
Expand Down Expand Up @@ -4754,18 +4842,31 @@
"enum": [
"mistral",
"custom",
"ollama"
"ollama",
"anthropic"
]
},
"apiUrl": {
"type": "string"
},
"apiKey": {
"type": "string",
"description": "Omit or leave empty to use the stored OCR_API_KEY / MISTRAL_API_KEY"
"description": "Omit or leave empty to use the stored OCR_API_KEY / MISTRAL_API_KEY (for anthropic, the stored Claude OCR key, then the Anthropic AI key)"
},
"setupOcrValidationTimeoutMs": {
"type": "integer"
},
"anthropicApiKey": {
"type": "string",
"description": "Anthropic AI key typed but not saved yet; used by anthropic when no OCR key is set"
},
"anthropicApiUrl": {
"type": "string",
"description": "Anthropic AI proxy URL typed but not saved yet"
},
"anthropicModel": {
"type": "string",
"description": "Anthropic AI model; anthropic returns it as suggestedModel"
}
}
}
Expand Down Expand Up @@ -4802,6 +4903,10 @@
"nullable": true,
"description": "Recommended default; absent for providers without model classification"
},
"live": {
"type": "boolean",
"description": "anthropic only — false when the live list failed and the suggested Claude models are returned instead"
},
"resolvedApiUrl": {
"type": "string"
},
Expand Down Expand Up @@ -5642,7 +5747,8 @@
"openai",
"ollama",
"custom",
"azure"
"azure",
"anthropic"
],
"example": "openai"
},
Expand Down Expand Up @@ -5686,6 +5792,58 @@
"description": "Model name for custom LLM provider",
"example": "custom-model"
},
"anthropicApiKey": {
"type": "string",
"description": "API key for Anthropic (required when aiProvider is 'anthropic' and none is stored; leave empty to keep the configured value). Saved only with aiProvider 'anthropic'; the anthropic* fields below likewise.",
"example": "sk-ant-api03-abc123"
},
"anthropicModel": {
"type": "string",
"description": "Claude model for analysis; empty keeps the configured one",
"example": "claude-sonnet-5-5"
},
"anthropicEffort": {
"type": "string",
"description": "Effort level; empty leaves it to the model. Rejected with 400 when it is not one of the listed levels.",
"enum": [
"",
"low",
"medium",
"high",
"xhigh",
"max"
],
"example": ""
},
"anthropicMaxTokens": {
"type": "integer",
"minimum": 1,
"description": "Completion cap per request, thinking included. Rejected with 400 when it is not a positive whole number; empty keeps the configured value.",
"example": 16000
},
"anthropicStructuredOutput": {
"type": "string",
"description": "Constrain the analysis answer to a JSON schema (yes/no)",
"enum": [
"yes",
"no"
],
"example": "yes"
},
"anthropicPromptCaching": {
"type": "string",
"description": "Mark the system prompt cacheable (yes/no)",
"enum": [
"yes",
"no"
],
"example": "yes"
},
"anthropicApiUrl": {
"type": "string",
"description": "Only for a proxy; empty uses api.anthropic.com",
"example": ""
},
"scanInterval": {
"type": "number",
"description": "Interval in minutes for scanning new documents",
Expand Down Expand Up @@ -5922,6 +6080,17 @@
"description": "How many entries the vocabulary proposed from the tag names should aim at, types and topics together (5-100, default 25). Out-of-range values are clamped, a non-numeric value keeps the current setting.",
"example": 25
},
"ocrProvider": {
"type": "string",
"description": "OCR fallback provider. For anthropic (Claude OCR) an empty ocrApiKey uses the Anthropic AI key, and ocrApiUrl is an optional proxy URL; a key or URL stored for another provider is cleared when switching to it.",
"enum": [
"mistral",
"custom",
"ollama",
"anthropic"
],
"example": "mistral"
},
"ocrAutoProcessEnabled": {
"type": "string",
"description": "Process queued OCR documents automatically (yes/no)",
Expand Down
8 changes: 8 additions & 0 deletions config/ai-provider-presets.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
"model": "gpt-4o-mini",
"tokenPlaceholder": "sk-..."
},
{
"id": "anthropic-default",
"label": "Anthropic (Claude)",
"provider": "anthropic",
"apiUrl": "",
"model": "claude-haiku-5-5",
"tokenPlaceholder": "sk-ant-..."
},
{
"id": "lmstudio-local",
"label": "LM Studio (OpenAI compatible)",
Expand Down
9 changes: 9 additions & 0 deletions config/changelog.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ const RELEASES = [
'New: Settings sections for Duplicates and Simplify tags',
],
},
{
version: 'v2026.10.01',
entries: [
'New: Anthropic (Claude) as AI provider. Settings and the setup wizard take an API key and list the Claude models it can use; Claude Haiku 5.5 is the default. The analysis comes back as structured output, and prompt caching keeps the repeated instructions cheap',
'New: Claude as OCR provider. It reads PDFs and images directly and uses the API key and model of the AI provider unless OCR has its own',
'Fix: Saving the settings no longer signs everybody out. The restart after a save created a new sign-in key, so every open session ended, and a page left open only showed vague errors such as "Model discovery failed". A session that ends now sends the page to the login screen',
'Fix: The manual tagging page uses the current design again. Its buttons, cards, document preview and tag chips were still built on styles that no longer exist',
],
},
];

const latestRelease = RELEASES[RELEASES.length - 1];
Expand Down
29 changes: 28 additions & 1 deletion config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ startupLog(logLevel, 'info', 'Configuration loaded:', {
});

module.exports = {
PAPERLESS_AI_VERSION: 'v2026.09.03',
PAPERLESS_AI_VERSION: 'v2026.10.01',
CONFIGURED: false,
configSourceMode: CONFIG_SOURCE_MODE,
getApiKey,
Expand Down Expand Up @@ -648,6 +648,33 @@ module.exports = {
deploymentName: process.env.AZURE_DEPLOYMENT_NAME || '',
apiVersion: process.env.AZURE_API_VERSION || '2023-05-15',
},
// The Claude API speaks its own Messages dialect, so it is a provider of
// its own rather than a preset of `custom`. Request rules per model live in
// services/anthropicSupport.js.
anthropic: {
apiKey: process.env.ANTHROPIC_API_KEY || '',
// Empty means api.anthropic.com; set only for a proxy in front of it.
apiUrl: process.env.ANTHROPIC_API_URL || '',
model: process.env.ANTHROPIC_MODEL || 'claude-haiku-5-5',
// low | medium | high | xhigh | max; empty leaves the model's default.
effort: String(process.env.ANTHROPIC_EFFORT || '')
.trim()
.toLowerCase(),
// Completion cap per request. Thinking counts toward it, which is why it
// is separate from RESPONSE_TOKENS.
maxTokens: parseTokenCount(
process.env.ANTHROPIC_MAX_TOKENS,
16000,
'ANTHROPIC_MAX_TOKENS'
),
// Constrain the analysis answer to a JSON schema (output_config.format).
structuredOutput: parseEnvBoolean(
process.env.ANTHROPIC_STRUCTURED_OUTPUT,
'yes'
),
// Mark the system prompt cacheable; it repeats for every document of a scan.
promptCaching: parseEnvBoolean(process.env.ANTHROPIC_PROMPT_CACHING, 'yes'),
},
mistralOcr: {
enabled: parseEnvBoolean(process.env.MISTRAL_OCR_ENABLED, 'no'),
provider: String(
Expand Down
Loading
Loading