[1.2.0] - 2026-09-09
Highlights
- Profile-aware viewer navigation, declared record fields, relationships, provenance and evidence drill-down. Stored source previews are distinguished from original-publication links; artifact bytes are freshly verified and served only on loopback. Narrow layouts, plain-language labels and initially fitted, yielding graph layouts improve browsing.
- OrcaRouter provider support with
openai/gpt-4o-minias its default and explicit credentials; embeddings remain unwired and fail closed. - Typed pages use the profile's declared title field. Citation line-list parsing is shared across consumers and accepts whitespace around commas.
- Invalid concurrency warnings use stderr, preserving the quickstart JSON output channel.
Contributors
Credit includes original proposals, reports and diagnoses, including work completed or extended by maintainers:
- @PipDscvr: profile-aware viewer and declared fields (#176, #177, #180, #189), Windows path fixes (#172), separate embedding providers (#174), and source removal (#179).
- @LorenzoGentile: optional Sources sections (#183), reasoning-model request compatibility (#184), and abbreviated-wikilink repair (#193).
- @Marc-oss-hub: OrcaRouter provider support (#182).
- @typevolant: citation line-list parsing and validation (#166), including the groundwork for #168.
- @TigerOfCountryYao: original embedding opt-out, additive system policy, and shared-page reconciliation contributions (#169, #170, #171), completed in #198, #195 and #199.
- @binyangzhu000-sudo: original Atlas Cloud provider contribution (#167), completed in #190.
- @suyunzzz: subscription-authentication request (#56), addressed through local CLI-session reuse in #205.
- @tienlx91: source-removal request (#60), implemented in #179.
- @carmilso: project-level compile-instructions request (#144), partly addressed by the SDK system policy in #195.
- @knew-inventai: separate embedding-endpoint request (#154), implemented in #174.
- @squ1ddy: Windows profile-path report and diagnosis (#163), addressed in #172.
- @graysoncooper: quickstart JSON-output corruption report (#191), addressed in #204.
- @ddiall: ingestion-quality feedback in the source-removal discussion (#60).
Known limitations
- Windows path fixes are included, but release CI remains Linux-only. Full Windows filesystem-confinement guarantees are not verified; use Linux for untrusted projects until #175 is resolved.
Added
-
OpenAI Codex CLI provider —
LLMWIKI_PROVIDER=codex-agentor
--provider codex-agentdelegates chat and structured generation to a locally
authenticated Codex CLI. The same one-run--provideroverride is now
available oncompile,refresh,query,watch,rules extract,eval,
andquickstart. Codex runs non-interactively in an ephemeral read-only
sandbox with a minimal environment, bounded output and process-tree lifetime,
interruption cleanup, and independently validated structured results. It
never reads project.envfiles or falls back to API-key authentication when
explicitly selected; embeddings require an explicit existing backend. -
Skip embeddings on compile —
compile({ embeddings: false })runs page generation, links and the lexical index without any embedding-provider call or pending-embedding retry, for an SDK host that maintains its own semantic index. Omitting it is unchanged.The flag closes a gap that is provider-specific. With
anthropicorclaude-agenta caller can already opt out by not settingVOYAGE_API_KEY, but theopenaiembedding credential falls back toOPENAI_API_KEYandollamaneeds no key at all, so those callers had no way to compile without embedding except to break chat.Contributed by @TigerOfCountryYao (#169).
-
Caller system policy on compile —
compile({ systemPolicy })appends deployment-specific editorial or publication guidance to the built-in compile prompts, for SDK hosts that need it without forking the prompts. It is additive rather than a replacement, sits before the source material, and blank or omitted leaves the prompt byte-identical.It is advisory rather than enforceable: a policy makes a model more likely to follow a rule, and nothing downstream verifies that it did. Anything that must hold belongs in a lint rule or a trust gate.
The policy is a prompt modifier, so changing or clearing it regenerates the pages compiled under the previous one, and replaces a pending review candidate produced under it. Each page records the policy's digest, never its text, since the modifier set travels into
state.json, page frontmatter and the JSON export.PROMPT_VERSIONadvances tov2.Groundwork by @TigerOfCountryYao (#170), and see #144 for the request this partly answers.
-
Atlas Cloud provider —
LLMWIKI_PROVIDER=atlascloud(aliasesatlas-cloud,atlas) routes chat and tool calls through the Atlas Cloud gateway, which exposes an OpenAI-compatible API across models from several publishers. Authenticate withATLASCLOUD_API_KEYorATLAS_CLOUD_API_KEY;ATLASCLOUD_BASE_URLoverrides the endpoint.Model ids are namespaced by publisher, and
compileextracts concepts through a tool call, so the default is a model Atlas Cloud lists as supporting tools. Embeddings are not wired up: the provider fails closed rather than inheriting OpenAI's semantics, so route them elsewhere withLLMWIKI_EMBEDDING_PROVIDERfor semantic search.Contributed by @binyangzhu000-sudo (#167).
-
Separate embedding provider —
LLMWIKI_EMBEDDING_PROVIDERselects the backend that serves embeddings, independently ofLLMWIKI_PROVIDER. This makes split setups possible, such as Claude Agent SDK for generation with a local vLLM instance serving embeddings over its OpenAI-compatible endpoint. Valid values areanthropic,claude-agent,openai, andollama.minimaxandcopilotexpose no embeddings API, and naming one now fails with a clear error listing the valid values instead of an opaque failure from the provider'sembed(). When the variable is set, the provider's own credential is required —VOYAGE_API_KEYforanthropicandclaude-agent,OPENAI_API_KEYforopenai— unlessOPENAI_EMBEDDINGS_BASE_URLpoints at a self-hosted endpoint, which needs no key. Behaviour is unchanged when the variable is unset.Thanks to @knew-inventai for the request (#154).
Changing the embedding backend invalidates the embedding index. It records the provider, model, and endpoint that produced its vectors, so the next
llmwiki compilere-embeds every page. The model name alone is not enough to tell two backends apart — a local server answering totext-embedding-3-smalland cloud OpenAI tag a store identically while producing vectors that do not share a space, and nothing downstream would notice. Moving betweenanthropicandclaude-agentdoes not rebuild: both embed via Voyage with the same model.Set
OPENAI_EMBEDDINGS_API_KEYto give a separate embeddings endpoint its own credential. Without it the embeddings client reusesOPENAI_API_KEY, which sends your cloud OpenAI key to whatever hostOPENAI_EMBEDDINGS_BASE_URLnames; llmwiki now warns when that happens, except onlocalhost. The warning redacts any credential carried in the endpoint URL itself, and the endpoint is hashed rather than stored verbatim in.llmwiki/embeddings.json.An index written before llmwiki recorded the endpoint carries only its model name. It is preserved while you run without
LLMWIKI_EMBEDDING_PROVIDERor an endpoint override, so upgrading does not re-embed an existing project; under either override the model name cannot establish where the vectors came from, so the next compile rebuilds the index once and records the full configuration from then on. -
llmwiki rm <source>— Deletes a source and the concept pages derived exclusively from it, leaving pages another source also contributed to untouched. There is no confirmation flag; preview first with--dry-run, which takes no lock and changes nothing.rmrefuses cleanly if another llmwiki process holds the project lock, and journals page deletes so a crash mid-removal recovers on the nextrmorcompile(#60).The plan is computed before the lock is taken, so
rmrecomputes which pages the source owns from fresh state under the lock and refuses the whole removal if anything moved in between — a page became shared, became exclusive, moved to another source, or was added. Nothing is deleted on a refusal; re-running plans against current state.llmwiki watchrecompiles on any change undersources/, so this window is an ordinary workflow rather than a rare schedule.The source file is deleted before the pages, so a failed page batch leaves the file gone and its state entry behind. Re-running
rmwith the same name recognizes that pairing as an interrupted removal and finishes the job, rather than reporting "no source matches" while the pages are still on disk. A name occupied by something that is not a valid source — a symlink, a directory — is not treated as an interrupted removal.A kept page's body still cites the removed source, which
llmwiki lintreports as abroken-citationerror.rmwarns whenever it keeps a page so the lint failure is not a surprise; it does not edit the citations.rmis now a reserved core CLI verb, alongside llmwiki's other top-level commands. A profile that declares a workflow keyedrmnow fails validation at load instead of installing; rename the workflow to use that profile. -
Optional
## Sourcessection —LLMWIKI_SOURCES_SECTION=off, or--no-sources-sectiononllmwiki compile, stops page generation from asking the model for a trailing## Sourcessection. Unset preserves the prompt byte-for-byte.This is for projects that render source attribution themselves. A page already carries its provenance twice — the
sources:frontmatter, which the compiler builds from the source files it actually read rather than from anything the model writes, and the inline^[file.md:1-5]citation markers — so a consumer that displays either one shows the same list a third time in the prose. Nothing downstream reads the section: it is a prompt instruction only, and no linter, exporter, or citation rule parses it.Suppressing the request avoids matching a localized heading downstream: under
--langthe model may translate## Sourcesalong with the page. This changes the prompt instruction; it does not enforce the absence of a heading in model output.Setting or clearing this preference regenerates affected pages through the existing prompt-modifier fingerprint. Page provenance records
sourcesSection=offwhen disabled.PROMPT_VERSIONadvances tov3to identify the implementation with a conditional Sources instruction; the default prompt text is unchanged.
Fixed
-
A shared page kept by
llmwiki rmcould never be rebuilt —rmrecords each kept slug instate.frozenSlugs, the same marker compile sets when it notices a deleted source, and that marker was terminal:mergeExtractionsskipped a frozen slug outright and nothing removed a slug from the set. The page kept the removed source's prose and its citations permanently,llmwiki lintreportedbroken-citationat error severity on a page no command could repair, and deleting the page to force a rebuild lost it for good.A slug frozen because the current run's extraction failed is still skipped and preserved. A slug carried in persisted state is now a reconciliation marker instead: the page is rebuilt from whatever owners survive, with the removed source's contribution dropped, and a page no live source owns is orphaned rather than held. Fixes #194.
Contributed by @TigerOfCountryYao (#171).
-
Interlink resolution corrupted prose containing
$sequences — when compile linked a page, it spliced the rewritten body back in with a string replacement, andString.replaceinterprets$&,$`,$'and$$there. A page readingThe PID is $$lost a$; one containing a backtick-dollar had its own frontmatter spliced into the middle of the sentence;$&duplicated the body. Shell, sed, awk and Makefile pages carry those sequences as ordinary prose. The rewritten body is now inserted verbatim. -
Changing the output language left existing pages untouched —
llmwiki compile --lang Japaneseover an already-compiled project reported "Nothing to compile" and every page kept its previous language, because change detection classified a source by the SHA-256 of its bytes alone and a prompt modifier is not part of the source. The selected modifiers are now recorded in.llmwiki/state.json, and flipping one invalidates the pages it would have changed. SettingLLMWIKI_OUTPUT_LANGhas the same effect as the flag.Pages also carry a
promptModifiersfrontmatter entry naming the modifiers active when they were generated, surfaced per page in the JSON export.promptVersionnames the prompt implementation and is identical whether or not a modifier was selected, so it could not tell two such pages apart.A project that never set a modifier pays nothing on upgrade: its first compile records "none selected" and finds no difference. A project already running with
--langwhen it upgrades recompiles once, because a state file predating this cannot establish what its pages were generated under. -
Wikilinks naming an existing page too briefly never resolved — page generation writes a concept's short canonical name while the page it means carries a longer descriptive title, so
[[Argo CD]]sat broken next toargo-cd-image-update-ownership-model. Extraction chooses page titles and generation chooses link text, independently and in that order, and nothing reconciled the two. Compile now runs a repair pass after interlink resolution that repoints a link when its slug prefixes exactly one page:[[Argo CD]]becomes[[argo-cd-image-update-ownership-model|Argo CD]].Only the link target is rewritten; visible labels, code examples, frontmatter, and literal dollar sequences are preserved. A slug prefixing two live or pending pages is left alone, as is one prefixing none. Pending-only matches wait for approval, which retries repairs. Reads use compile's confined reader, dropping escaping paths and special files before their bytes can enter a write.
Measured across five compiles of a mixed corpus (two PDFs plus five prose documents), this repaired 21.3% of broken wikilinks on a GPT-5-class model and 16.7% on
gpt-4o-mini, with no page's prose changed. -
Reasoning models could not be used at all — the OpenAI provider hard-coded
max_tokenson all three completion paths, and the o-series and GPT-5 families reject it:Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead.The failure came back on the first extraction request, before any page was written, so those models were unusable rather than degraded. The field is now selected from the model id, withLLMWIKI_OPENAI_TOKEN_PARAMto force it for gateways that serve a reasoning model under a private id. The SDK has carriedmax_tokensas deprecated in favour ofmax_completion_tokenssince 6.x.GPT-5.6 models default to
reasoning_effort: nonefor Chat Completions tool compatibility. Older families keep their server default because they do not all acceptnone.LLMWIKI_OPENAI_REASONING_EFFORToverrides the effort with any ofnone,minimal,low,medium,high, orxhigh; the chosen model must support that value. These options apply to the shared OpenAI-compatible request paths, including completion, streaming, and tool calls.The default is deliberately not applied to the o-series: it accepts a tool-carrying request with the field absent, and rejects some of the values above, so guessing on its behalf would turn a working request into a 400. Set the variable to opt it in.
An unrecognised value for either variable fails on the first request attempt with the accepted values named, before network access and without retry backoff. Models outside the detected reasoning families retain their previous request shape.
-
Windows: profile path validation rejected every declared directory — on win32,
llmwiki template initfailed for every template withentity directory must be under 'wiki/', any profile declaring a workflowprojectionFilefailed to load, and an entity directory declared aswiki/was wrongly accepted despite containing every reserved subtree — on win32 it was the only entity directory that loaded at all. Declared directories canonicalize to/-joined repo-relative paths, but the containment check built its prefix with the platform separator (\on Windows), so no nested path ever matched. The lexical profile-path checks now compare POSIX paths directly; native path confinement is unchanged. Reported and diagnosed by @squ1ddy (#163). -
Windows: broken links in the generated wiki index — the same separator bug on the output side. Entity-page links in
wiki/index.mdare built frompath.relative, which emits\on win32, so a NESTED entity directory produced the unusable linkresearch\papers/foo.md. Link targets are now normalized to POSIX. Single-level directories were unaffected, which is why this went unnoticed (#163). -
Windows: native separators in public problem paths — the same separator bug one layer further out, on the reported-problem surface.
EntityProblemView.pathis documented as project-relative portable content, but both producers returnedpath.relativeoutput raw, so on win32llmwiki status, the viewer, context packs, and the JSON export reportedwiki\notes\untitled.mdwhere the contract promiseswiki/notes/untitled.md. Both now normalize to POSIX. The regression gate was widened to match: instead of naming the two symbols the first fix touched, it now requires everypath.relativein the lexical profile layer to be routed throughtoPosixPath(#163). -
npm testcould not run on Windows at all — vitest's global setup shelled out tonpx, which isnpx.cmdthere and has not been resolvable bychild_processwithoutshell: truesince the Node 22 hardening for CVE-2024-27980. The setup threw, collection aborted, and vitest reported the unrelated "No test files found". It now invokes the build directly with the running Node binary, needing no shell on any platform. -
Embedding store dimensions after a full rebuild — when the embedding model changed, the rebuilt store carried the previous vector dimension forward, so switching to a provider whose vectors have a different dimension failed validation on every subsequent compile and never recovered. The rebuilt store now takes its dimension from the newly written vectors.
-
A rebuilt-but-empty embedding index broke every query — a rebuild with nothing eligible to embed persisted a store declaring
dimensions: 0, and each later query asserted its query vector against that zero and threw. No compile rewrote the store, so it never recovered. A non-positive stored dimension is now treated as unknown, and a read with no candidates returns before embedding the query at all — which also drops a provider round-trip that could only be scored against an empty pool. -
OPENAI_EMBEDDINGS_API_KEYwas accepted at startup and then ignored — the embeddings client was built only whenOPENAI_EMBEDDINGS_BASE_URLwas also set, so a configuration supplying just the dedicated key passed validation and then authenticated with the chat client's placeholder credential, failing later as a 401. The dedicated client is now built whenever either the endpoint or the key is configured. -
A misconfigured
LLMWIKI_EMBEDDING_PROVIDERfailed late and inconsistently — validation ran inside the embedding call, so the same typo madellmwiki queryexit 1, made context retrieval degrade, and made compile warn, retry, and eventually quarantine the affected pages. An unusable name also fell through to the default model and was reported as "the index was built with a different model", which described the wrong problem. The provider guard now checks it at startup, before any work begins, alongside the chat provider's credentials.