Skip to content

Simplify LLM API key configuration#6

Merged
rejojer merged 2 commits into
devfrom
simplify-llm-api-key-config
Apr 7, 2026
Merged

Simplify LLM API key configuration#6
rejojer merged 2 commits into
devfrom
simplify-llm-api-key-config

Conversation

@rejojer

@rejojer rejojer commented Apr 7, 2026

Copy link
Copy Markdown
Member

Summary

  • Remove llm_api_key_env from config, hardcode reading LLM_API_KEY env var
  • Simplifies setup: users just set LLM_API_KEY in .env, no config field needed
  • Provider-specific env vars (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) still work via LiteLLM auto-detection
  • okb init has one less interactive prompt

Test plan

  • All config tests pass (test_config.py 6/6)
  • Verified LLM_API_KEY works with OpenAI, Anthropic, and Gemini
  • Verified provider-specific env vars still work without LLM_API_KEY

rejojer added 2 commits April 8, 2026 06:14
- Hardcode reading LLM_API_KEY env var instead of indirecting through config
- Remove llm_api_key_env from DEFAULT_CONFIG, okb init prompts, and config.yaml
- Provider-specific env vars (OPENAI_API_KEY, etc.) still work via LiteLLM auto-detection
- One less config field, one less okb init step
@rejojer
rejojer force-pushed the simplify-llm-api-key-config branch from c781a30 to 55444d3 Compare April 7, 2026 22:15
@rejojer

rejojer commented Apr 7, 2026

Copy link
Copy Markdown
Member Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

@rejojer
rejojer merged commit 018342e into dev Apr 7, 2026
@rejojer
rejojer deleted the simplify-llm-api-key-config branch April 7, 2026 22:24
KylinMountain added a commit that referenced this pull request May 24, 2026
Architectural review (4 parallel Opus auditors) found that the skill_runner
core was already generic, but the deck SURFACE was still fused to
Editorial Monocle. Fixed:

* validator: now takes optional `grammar` param (DeckGrammar TypedDict);
  skill-agnostic by default (only checks file present, parses, ≥5
  slides, self-contained). Third-party deck skills (guizang, swiss)
  now pass validation cleanly. Editorial-specific rules opt-in via
  `EDITORIAL_MONOCLE_GRAMMAR`. (finding #2)
* skills/openkb-deck-editorial/SKILL.md: declares its grammar +
  output_path_template under `od:` frontmatter — `run_skill` reads
  these and applies them post-run.
* run_skill: now honors frontmatter `od.mode`, `od.output_path_template`,
  `od.deck_grammar`. When mode=="deck" and template is set, the runner
  injects the path into intent, verifies the file exists post-run, and
  runs validate_deck with the skill's grammar. Validation result is
  returned via new SkillRunResult dataclass. (findings #4, #5)
* `openkb deck new --skill <name>`: CLI flag accepts any installed deck
  skill (default openkb-deck-editorial). guizang and swiss now usable
  from the scripted CLI, not only freeform chat. (finding #1)
* `/deck new --skill <name>` chat slash: same flag, parsed positionally
  alongside --critique. (finding #1)
* tests/test_read_kb_file.py: 13 new tests mirroring test_write_kb_file
  for the read-side allow-list. Pins refusal of `.openkb/config.yaml`,
  `.env`, `raw/`, `..` traversal, absolute paths. (finding #6)
* Generator deck branch: no longer calls validate_deck directly; just
  propagates run_deck_create's SkillRunResult.validation up. Validation
  is now a property of "this skill declared mode=deck", not of "this
  CLI path was taken".

Existing tests updated:
* tests/test_deck_validator.py: explicit grammar arg on Editorial-
  specific tests; added test_guizang_shape_passes_generic_mode +
  test_missing_cover_ignored_in_generic_mode to pin both modes.
* tests/test_deck_creator.py: mocks return SkillRunResult; new
  test_run_deck_create_honors_skill_name_override for --skill flag.
* tests/test_generator.py: deck dispatch test mocks SkillRunResult.

Below-threshold findings deferred:
* Generator if/else → registry (score 70) — works, just not extensible
  via plugin; future.
* Iteration backup in chat freeform path (score 75) — needs write_kb_file
  hook; separate change.
* run_skill / scan_local_skills / _handle_slash_critique direct tests
  (scores 60-70) — covered indirectly by integration; can add later.

Regression: 538 tests pass (was 523 pre-fix; net +15 = 13 new
read_kb_file tests + 2 new validator-mode tests).
KylinMountain added a commit that referenced this pull request Jul 21, 2026
…es (#198)

* feat(api): delete a knowledge base (POST /api/v1/kb/delete + openkb delete-kb)

Physical, irreversible KB deletion with a type-the-name confirmation.

- config.delete_kb: rmtree the KB directory + unregister it from the global
  registry (known_kbs / kb_aliases / default_kb, via the new unregister_kb).
  Guards against deleting a non-KB path; tolerates a ghost registry entry
  (directory already gone) by just unregistering.
- POST /api/v1/kb/delete: confirm_name must equal kb, re-checked server-side.
  Lives in a new api_kbs_router (sibling of the config/graph/output routers) so
  api.py stays under the 800-line gate; delete_kb self-locks (config lock), no
  create_app closure needed.
- openkb delete-kb NAME: type-the-name prompt (or --yes) then delete.

Tests: endpoint (success + unregister, confirm mismatch, non-KB target) in
test_api.py; config (physical delete + unregister, refuse non-KB, ghost
tolerance) in test_config.py.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG

* feat(api): delete a wiki page (POST /api/v1/page/delete) with backlink impact

Delete a concept/entity page and degrade references cleanly instead of leaving
them dangling:

- page_ops.delete_wiki_page: dry_run reports the backlink pages (whose inbound
  [[wikilinks]] would be demoted) without touching anything; execute removes the
  page under the KB ingest lock, strips its index.md entry outright
  (compiler.remove_doc_from_index — the entry is a [[link]] line), and demotes
  the now-dangling inbound [[links]] to plain text in ONLY those backlink pages
  (lint.fix_broken_links restrict_to). The page ref is "section/stem", validated
  to concepts/ or entities/ only (path-traversal-safe).
- New api_pages_router hosts POST /api/v1/page (read — relocated here from
  api.py to keep it under the 800-line gate) + POST /api/v1/page/delete.

Tests: dry-run impact + execute (page gone, inbound link demoted to text, index
entry removed, sibling kept), 404 on missing page, and rejection of unsafe /
non-editable refs.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG

* feat(api): edit a wiki page (PUT /api/v1/page) + link context (POST /api/v1/page/links)

- page_ops.edit_wiki_page: replace a concept/entity page's BODY while preserving
  its code-managed OKF frontmatter (type/description/sources) verbatim; any
  frontmatter in the submitted content is dropped. Dead [[wikilinks]] the user
  typed are demoted to plain text on save (OpenKB keeps the wiki broken-link-free)
  and returned as ghosts_stripped so the UI can warn. Atomic write under the KB
  ingest lock.
- page_ops.page_link_context (POST /api/v1/page/links): outbound + inbound links
  for the edit-impact panel. Editing the body does not break either (links are
  path-based), so this is context, not a blocker — the honest impact model.
- PUT /api/v1/page added to api_pages_router.

Tests: edit preserves frontmatter + keeps a resolvable link + demotes a dead one
(reports ghosts_stripped) + replaces the body; 404 on a missing page; links
reports out/backlinks.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG

* feat(web): delete a knowledge base from the settings sheet (type-name confirm)

Adds a Danger Zone section to KbSettingsSheet: a type-the-name confirmation
gates deleteKb() (POST /api/v1/kb/delete); on success KbDetail navigates back
to the KB list. New deleteKb client + kbSettings danger* keys + common cancel
(zh + en, identical key sets).

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG

* feat(web): in-reader page edit + delete with impact preview (F2/F3)

For an open concept/entity page, the reader header now shows Edit and Delete:
- Delete: a dry-run first (deletePage dryRun) surfaces the backlink pages whose
  [[links]] will demote to plain text; a red confirm card lists them, then the
  real delete closes the page and refreshes the inventory.
- Edit: a body textarea (frontmatter is preserved server-side), Save via
  PUT /api/v1/page, a links panel (getPageLinks: out/backlinks), a "recompile
  may overwrite" note, and a toast listing any dead links demoted to text.

Adds the deletePage/editPage/getPageLinks wiki clients and kb:pageOps.* locale
keys (zh + en, identical key sets). Build green (i18n guard + tsc + vite).

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG

* fix(workbench): address code-review findings (content-mgmt correctness + concurrency)

Backend (page_ops / kb_admin / lint):
- delete/edit now do their read-modify-write INSIDE the KB ingest lock and
  re-check the page exists under it: no stale backlink snapshot, no resurrecting
  a concurrently-deleted page, no 500 on a racing unlink (missing_ok=True). [#4,#5,#6]
- delete_kb serializes rmtree under the ingest lock (was fully unlocked). Moved
  delete_kb/unregister_kb into a new openkb/kb_admin.py so config.py drops back
  under the per-file line gate (751 lines). [#1,#15]
- page deletion no longer scans or rewrites lint's _EXCLUDED_FILES
  (AGENTS.md/SCHEMA.md/log.md); fix_broken_links(restrict_to) also refuses them,
  which protects `openkb remove` too. [#2]
- edit_wiki_page treats `content` as the body verbatim — no naive frontmatter
  split that silently dropped a body opening with a '---' block. [#3]
- delete uses the real on-disk stem for the exact-match index-entry removal
  (case-insensitive FS), and adds index.md to the demotion set so a [[target]]
  embedded in another entry's brief no longer dangles. [#7,#9]

API / CLI:
- delete-KB endpoint & CLI can now clean a ghost registry entry (registered name
  whose directory was removed by hand), catch delete_kb's ValueError -> 400, and
  the endpoint is typed `-> KbDeleteResponse`. [#8,#13,E5]

Frontend (KbDetail):
- Deleting a KB dispatches `openkb:reload-kbs` so the sidebar refreshes
  immediately (was stale until a manual reload) — live-test fix.
- Removed the dead `pageReloadSeq` path (editPage always returns content). [#14]

Tests: excluded-doc-untouched, '---'-body preservation, ghost-KB unregister;
non-KB-target test updated to the new resolution model. Backend 1234 passed,
mypy/ruff clean, frontend build green.

Not changed (intentional): pages_linking_to is NOT folded into build_graph (that
excludes explorations/ backlinks) [#10]; the preview+confirm delete inherently
scans per request [#11]; EDITABLE_SECTIONS stays duplicated in the frontend,
which can't import the Python constant [#12].

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG

* feat(workbench): allow editing summary pages (edit ⊇ delete section sets)

Summaries are compiled markdown like concepts/entities (a recompile regenerates
them), so they are now editable via PUT /api/v1/page. They remain NOT
independently deletable — a summary is removed by deleting its source document.

Split page_ops into EDITABLE_SECTIONS (concepts/entities/summaries) vs
DELETABLE_SECTIONS (concepts/entities); validate_page_ref takes the allowlist.
Frontend ReaderBody gates Edit on canEdit (incl. summaries), Delete on canDelete.

Test: summary editable (frontmatter preserved) + summary delete rejected (400).

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG

* fix(api): cross-platform delete_kb + endpoint OSError handling (xhigh review #1,#2)

- delete_kb no longer holds the ingest lock DURING rmtree (the lock file lives
  inside the KB dir; Windows cannot delete an open file — the prior review-fix
  regressed this). It now takes the lock as a BARRIER (drain + wait out any
  in-flight mutation), releases it, re-checks existence, then rmtrees. [#1]
- delete-KB endpoint maps FileNotFoundError to an idempotent success (a
  concurrent delete already removed the tree) and other OSError to a clean 500
  with a message, instead of an uncaught 500 stack trace. [#2]

Tests: endpoint OSError -> clean 500, FileNotFoundError -> 200 deleted.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
KylinMountain added a commit that referenced this pull request Jul 22, 2026
…y-list inherit, silent reads, set-diff

- config: define DEFAULT_ENTITY_TYPES before DEFAULT_CONFIG and add
  `entity_types` to DEFAULT_CONFIG so the key layers like every other
  GLOBAL_SCALAR_KEY and always appears in the effective config (#1).
- config: resolve_effective_config treats an empty entity_types list the
  same as null → inherit, so a KB that cleared its override doesn't pin an
  empty vocabulary (#2).
- config: resolve_entity_types(config, *, warn=True); config-read paths pass
  warn=False so a plain GET doesn't spam coercion warnings (#4).
- api_config: both read paths call resolve_entity_types(..., warn=False).
- KbSettingsSheet: inherited badge shows the effective list, not
  `globalValue ?? effective`; drop the now-unused globalValue prop (#3).
- Settings: global entity_types diff is order-insensitive — the vocabulary
  is a set, so re-adding a removed type is not a change (#6).

Backend pytest 1240 passed; ruff/format/mypy clean; frontend build green.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
KylinMountain added a commit that referenced this pull request Jul 22, 2026
…ane (#197) (#199)

* feat(web): read a document's converted source text in the Documents pane (#197)

The Documents pane listed each ingested doc (name/type/hash) with no way to
read the converted full text ingestion produced under wiki/sources/.

Backend: new POST /api/v1/document/source resolves a doc hash to its source
text — short docs read <doc_name>.md; long docs concatenate the per-page
<doc_name>.json (page text joined by a thematic break). Hash is the identifier
(unique, avoids doc_name/stem collisions); resolution prefers the registry's
stored source_path then falls back to the wiki/sources/<doc_name>.{md,json}
convention, with a path-traversal guard. Read-only (sources are do-not-edit).

Frontend: document rows are now clickable and open a wide read-only slide-out
reader (MarkdownView) — ESC/overlay/close to dismiss, independent scroll,
content cached + memoized per hash, native find-in-page preserved (no
virtualization). Delete stays inline (stopPropagation). Closed drawer is inert.

Known limitation: images embedded in long-doc pages are not rendered inline yet.

* fix(web): address xhigh code-review findings for the document reader (#197)

Correctness / a11y:
- Rebuild the reader drawer on Radix Dialog (like KbSettingsSheet) instead of
  a hand-rolled overlay: proper modal focus trap, initial + return focus,
  Escape, and background inert (was: aria-modal with none of it) [#4]. This
  also removes the hand-rolled window keydown listener that re-subscribed every
  render [#7].
- Restructure each document row so the open-reader target is a real <button>
  and the delete control is a SIBLING, not nested. Keyboard-activating delete
  no longer bubbles into opening the reader, and the invalid nested-interactive
  markup is gone [#1, #5].
- Resolve a source file by the doc's own type (long → .json first, else .md),
  so two docs sharing a doc_name each resolve to their own file rather than
  whichever extension is tried first [#2].
- Guard source reads: skip non-dict page entries, reject non-list JSON, and
  return a controlled 500 on corrupt/unreadable sources instead of an
  unhandled exception [#3].
- Invalidate the per-hash content cache when the inventory changes, so a
  reopen after recompile refetches instead of serving stale text [#6].

Fetch/cache/memoized body moved to DocumentsPane so they survive the drawer's
unmount-on-close. #8 (frontmatter stripping) intentionally not applied: source
docs render verbatim (a user's own frontmatter is content, unlike wiki-page OKF
metadata). Adds tests for the collision and malformed-JSON paths.
KylinMountain added a commit that referenced this pull request Jul 22, 2026
…verride) (#200)

* feat(api): configurable entity_types (global + per-KB) via the config API

entity_types (the entity-extraction vocabulary) is now surfaced through the
config read/write API, layering global.yaml -> KB config.yaml like the other
scalars: a KB list overrides the global list wholesale, an explicit null
inherits, and unset falls back to DEFAULT_ENTITY_TYPES. The compiler already
consumed config["entity_types"] via resolve_entity_types; this just exposes it.

- GLOBAL_SCALAR_KEYS gains "entity_types" (layering + per-key `sources` tracking;
  the value-not-None-wins rule is type-agnostic, so it works for a list).
- _KbConfigWritable / GlobalConfigValues / KbConfigResponse / GlobalConfigResponse
  carry entity_types; read_kb_config/read_global_config report the cleaned
  EFFECTIVE list (resolve_entity_types) plus the raw global value for the badge.
- PATCH /api/v1/kb/config and PATCH /api/v1/config accept entity_types.

Tests: KB override (cleaned + source 'kb') + null revert, global patch, global
inheritance; updated the global-defaults shape assertion. Frontend UI follows.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG

* feat(web): entity-types config UI — chips editor (global default + per-KB override)

- EntityTypesEditor: shared controlled chips editor (Enter/comma to add, x to
  remove; "other" is a fixed always-included chip; IME-safe composition).
- KbSettingsSheet: an EntityTypesRow with the same inherit/override Switch as the
  scalar rows — turning override on seeds+persists the KB's own list, off reverts
  via null; inherited state shows the global/default list as a badge. Each chip
  change persists and adopts the server-cleaned response.
- Settings (general tab): a global entity-types chips editor, order-sensitive
  diff into the save patch (joins the existing dirty/SaveBar flow).
- "changes affect future recompiles only" note on both surfaces.

New keys in common/kbSettings/settings (zh + en, identical sets). Build green
(i18n guard OK). Backend was committed in 92f8f41.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG

* fix(entity-types): address xhigh review — DEFAULT_CONFIG parity, empty-list inherit, silent reads, set-diff

- config: define DEFAULT_ENTITY_TYPES before DEFAULT_CONFIG and add
  `entity_types` to DEFAULT_CONFIG so the key layers like every other
  GLOBAL_SCALAR_KEY and always appears in the effective config (#1).
- config: resolve_effective_config treats an empty entity_types list the
  same as null → inherit, so a KB that cleared its override doesn't pin an
  empty vocabulary (#2).
- config: resolve_entity_types(config, *, warn=True); config-read paths pass
  warn=False so a plain GET doesn't spam coercion warnings (#4).
- api_config: both read paths call resolve_entity_types(..., warn=False).
- KbSettingsSheet: inherited badge shows the effective list, not
  `globalValue ?? effective`; drop the now-unused globalValue prop (#3).
- Settings: global entity_types diff is order-insensitive — the vocabulary
  is a set, so re-adding a removed type is not a change (#6).

Backend pytest 1240 passed; ruff/format/mypy clean; frontend build green.

Claude-Session: https://claude.ai/code/session_01XMxbhmAkxxVV8CFWCZDBaG
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.

1 participant