lorekeep 0.8.4__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lorekeep-0.8.4 → lorekeep-0.9.0}/.gitignore +2 -0
- lorekeep-0.9.0/.release-please-manifest.json +3 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/CHANGELOG.md +7 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/PKG-INFO +1 -1
- {lorekeep-0.8.4 → lorekeep-0.9.0}/pyproject.toml +1 -1
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/__init__.py +1 -1
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/backup.py +2 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/cli.py +109 -21
- lorekeep-0.9.0/src/lorekeep/providers.py +128 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_backup.py +3 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_init_cli.py +36 -12
- lorekeep-0.9.0/tests/test_providers.py +159 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/uv.lock +4 -4
- lorekeep-0.8.4/.release-please-manifest.json +0 -3
- lorekeep-0.8.4/tests/test_providers.py +0 -22
- {lorekeep-0.8.4 → lorekeep-0.9.0}/.commitlintrc.json +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/.github/workflows/auto-merge-release-please.yml +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/.github/workflows/ci.yml +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/.github/workflows/lint-commits.yml +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/.github/workflows/release-please.yml +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/.lorekeep/config.yaml.example +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/.lorekeep/schema.json +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/.pre-commit-config.yaml +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/AGENTS.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/CLAUDE.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/LICENSE +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/README.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/SECURITY.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/cover.jpeg +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/README.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/architecture/agent.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/architecture/data-model.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/architecture/evaluation.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/architecture/journal.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/architecture/overview.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/architecture/permission.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/architecture/pipeline.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/architecture/serve-mcp.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/architecture/temporal.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/guides/backup.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/guides/compile.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/guides/data-home.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/guides/getting-started.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/guides/import.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/guides/serve.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/guides/wiki.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/superpowers/plans/2026-06-26-lorekeep-dotdir-backup.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/docs/superpowers/specs/2026-06-26-lorekeep-dotdir-layout-design.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/release-please-config.json +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/scripts/check-conventional-commit.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/agent.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/compile/__init__.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/compile/extract.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/compile/ingest.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/compile/providers.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/compile/resolve.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/compile/writer.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/config.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/defaults.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/eval/__init__.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/eval/construction.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/eval/gold.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/eval/locomo.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/eval/retrieval.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/facts_io.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/importer/__init__.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/importer/claude.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/importer/codex.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/importer/cursor.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/importer/opencode.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/integrations/__init__.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/integrations/claude_code.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/integrations/codex.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/integrations/common.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/integrations/cursor.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/integrations/detect.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/integrations/opencode.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/journal.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/mcp_server.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/models.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/paths.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/perm/__init__.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/perm/ns.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/pipeline.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/schema_io.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/store/__init__.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/store/fts.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/store/graph.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/src/lorekeep/wiki.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/conftest.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/fixtures/claude-session/memory/MEMORY.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/fixtures/claude-session/memory/lorekeep-design.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/fixtures/claude-session/memory/lorekeep-project-state.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/fixtures/claude-session/test.jsonl +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/fixtures/gold/payments.facts.jsonl +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/fixtures/raw/backend/payments.md +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/fixtures/retrieval/questions.json +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/fixtures/schema.json +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_agent_cli.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_agent_detect.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_backup_cli.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_compile_cli.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_config.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_construction_eval.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_defaults.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_determinism.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_doctor_cli.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_eval_cli.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_extract.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_fts.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_graph_store.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_import_claude.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_import_codex.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_import_cursor.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_import_opencode.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_ingest.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_init_chain.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_integrations.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_locomo_eval.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_mcp_add_cli.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_mcp_reload.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_mcp_server.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_mcp_write_tools.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_merge_journals.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_models.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_paths.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_perm.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_pipeline.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_resolve.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_retrieval_eval.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_schema_io.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_scope_awareness.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_serve_cli.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_session_hook.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_smoke.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_watch_e2e.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_watch_sessions.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_wiki.py +0 -0
- {lorekeep-0.8.4 → lorekeep-0.9.0}/tests/test_writer.py +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.0](https://github.com/manhhailua/lorekeep/compare/v0.8.4...v0.9.0) (2026-07-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* interactive provider/model picker with litellm catalog ([#89](https://github.com/manhhailua/lorekeep/issues/89)) ([ff915f5](https://github.com/manhhailua/lorekeep/commit/ff915f590c7de72f23e8ce9e8f82453a0a69763d))
|
|
9
|
+
|
|
3
10
|
## [0.8.4](https://github.com/manhhailua/lorekeep/compare/v0.8.3...v0.8.4) (2026-06-30)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -498,21 +498,93 @@ def _interactive_init(p: dict) -> tuple[str, str, str]:
|
|
|
498
498
|
so the caller can write the first file ``raw/<ns>/about.md``.
|
|
499
499
|
"""
|
|
500
500
|
import yaml
|
|
501
|
+
from lorekeep.providers import (
|
|
502
|
+
list_models, search_providers,
|
|
503
|
+
format_cost, is_dynamic, POPULAR,
|
|
504
|
+
)
|
|
501
505
|
|
|
502
506
|
typer.echo("\n=== Lorekeep setup ===\n")
|
|
503
507
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
)
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
typer.echo(f"
|
|
508
|
+
# ── Provider selection ─────────────────────────────────────────────
|
|
509
|
+
typer.echo("Popular providers:")
|
|
510
|
+
for i, prov in enumerate(POPULAR, 1):
|
|
511
|
+
typer.echo(f" {i}. {prov}")
|
|
512
|
+
typer.echo(f" {len(POPULAR) + 1}. [Search all providers]")
|
|
513
|
+
typer.echo(f" {len(POPULAR) + 2}. [Skip — configure later]")
|
|
514
|
+
|
|
515
|
+
choice = typer.prompt("\nChoice", default="1")
|
|
516
|
+
|
|
517
|
+
idx = int(choice) if choice.isdigit() else 0
|
|
518
|
+
if idx == len(POPULAR) + 2 or choice.lower() == "skip":
|
|
519
|
+
typer.echo(" → Skipped (edit config.yaml to add a provider later)\n")
|
|
520
|
+
ns = typer.prompt("Default namespace", default="private")
|
|
521
|
+
name = typer.prompt("Your name", default="")
|
|
522
|
+
bio = typer.prompt("Bio (one-line intro)", default="")
|
|
523
|
+
_write_config(p, backend="openai", model="gpt-4o-mini",
|
|
524
|
+
api_base=None, api_key_env=None, api_key=None, ns=ns)
|
|
525
|
+
return ns, name, bio
|
|
526
|
+
|
|
527
|
+
if idx == len(POPULAR) + 1 or choice.lower() == "search":
|
|
528
|
+
query = typer.prompt("Type provider name to search", default="")
|
|
529
|
+
from lorekeep.providers import list_providers
|
|
530
|
+
all_providers = list_providers()
|
|
531
|
+
results = search_providers(query, all_providers)
|
|
532
|
+
if not results:
|
|
533
|
+
typer.echo(" → No matches. Using default (openai).")
|
|
534
|
+
provider_name = "openai"
|
|
535
|
+
else:
|
|
536
|
+
typer.echo("")
|
|
537
|
+
for i, (prov, count) in enumerate(results[:20], 1):
|
|
538
|
+
typer.echo(f" {i}. {prov} ({count} models)")
|
|
539
|
+
sub = typer.prompt("Choice", default="1")
|
|
540
|
+
sub_idx = int(sub) if sub.isdigit() else 1
|
|
541
|
+
provider_name = results[min(sub_idx - 1, len(results) - 1)][0]
|
|
542
|
+
elif 1 <= idx <= len(POPULAR):
|
|
543
|
+
provider_name = POPULAR[idx - 1]
|
|
544
|
+
else:
|
|
545
|
+
provider_name = "openai"
|
|
510
546
|
|
|
511
|
-
|
|
547
|
+
typer.echo(f" → {provider_name}\n")
|
|
512
548
|
|
|
513
|
-
|
|
514
|
-
if
|
|
515
|
-
|
|
549
|
+
# ── Model selection ────────────────────────────────────────────────
|
|
550
|
+
if is_dynamic(provider_name):
|
|
551
|
+
model = typer.prompt(
|
|
552
|
+
f"Model name (free-text for {provider_name})",
|
|
553
|
+
default="llama3.2" if provider_name == "ollama" else "",
|
|
554
|
+
)
|
|
555
|
+
api_base = typer.prompt(
|
|
556
|
+
"API base URL", default="http://localhost:11434" if provider_name == "ollama" else "",
|
|
557
|
+
) or None
|
|
558
|
+
else:
|
|
559
|
+
models = list_models(provider_name)
|
|
560
|
+
if models:
|
|
561
|
+
typer.echo("Models (chat, sorted by cost):")
|
|
562
|
+
for i, m in enumerate(models[:20], 1):
|
|
563
|
+
fc = format_cost(m.input_cost)
|
|
564
|
+
fc_out = format_cost(m.output_cost)
|
|
565
|
+
ctx = f"{m.max_input_tokens // 1000}K" if m.max_input_tokens else "?"
|
|
566
|
+
typer.echo(f" {i}. {m.model} in={fc} out={fc_out} ctx={ctx}")
|
|
567
|
+
typer.echo(f" {len(models) + 1}. [Type custom model name]")
|
|
568
|
+
mchoice = typer.prompt("Choice", default="1")
|
|
569
|
+
midx = int(mchoice) if mchoice.isdigit() else 1
|
|
570
|
+
if 1 <= midx <= len(models):
|
|
571
|
+
model = models[midx - 1].model
|
|
572
|
+
elif midx == len(models) + 1:
|
|
573
|
+
model = typer.prompt("Model name (litellm string)", default="")
|
|
574
|
+
else:
|
|
575
|
+
model = models[0].model
|
|
576
|
+
else:
|
|
577
|
+
model = typer.prompt("Model name (litellm string)", default="")
|
|
578
|
+
api_base = None
|
|
579
|
+
|
|
580
|
+
typer.echo(f" → {model}\n")
|
|
581
|
+
|
|
582
|
+
# ── API key (skip for local providers) ─────────────────────────────
|
|
583
|
+
env_var = None
|
|
584
|
+
if is_dynamic(provider_name):
|
|
585
|
+
typer.echo(" → No API key needed for local provider.\n")
|
|
586
|
+
api_key = None
|
|
587
|
+
else:
|
|
516
588
|
api_key = typer.prompt(
|
|
517
589
|
"API key (saved into the gitignored config.yaml)",
|
|
518
590
|
default="",
|
|
@@ -521,22 +593,39 @@ def _interactive_init(p: dict) -> tuple[str, str, str]:
|
|
|
521
593
|
if api_key:
|
|
522
594
|
typer.echo(" → key stored in config.yaml\n")
|
|
523
595
|
else:
|
|
524
|
-
typer.
|
|
525
|
-
|
|
526
|
-
|
|
596
|
+
env_var = typer.prompt(
|
|
597
|
+
"API key env var name (or skip)",
|
|
598
|
+
default=f"{provider_name.upper().replace('-', '_')}_API_KEY",
|
|
599
|
+
)
|
|
600
|
+
if env_var.lower() not in ("skip", ""):
|
|
601
|
+
typer.echo(f" → set {env_var} before compiling\n")
|
|
602
|
+
else:
|
|
603
|
+
env_var = None
|
|
604
|
+
typer.echo(" → skipped (add key to config.yaml later)\n")
|
|
605
|
+
|
|
606
|
+
# ── Namespace + profile ────────────────────────────────────────────
|
|
607
|
+
ns = typer.prompt("Default namespace", default="private")
|
|
608
|
+
name = typer.prompt("Your name", default="")
|
|
609
|
+
bio = typer.prompt("Bio (one-line intro)", default="")
|
|
610
|
+
|
|
611
|
+
_write_config(
|
|
612
|
+
p, backend="openai", model=model, api_base=api_base,
|
|
613
|
+
api_key_env=env_var if not api_key else None,
|
|
614
|
+
api_key=api_key, ns=ns,
|
|
615
|
+
)
|
|
616
|
+
return ns, name, bio
|
|
527
617
|
|
|
528
|
-
ns = typer.prompt("Default namespace", default="me")
|
|
529
|
-
name = typer.prompt("Your name (what the agent calls you)", default="")
|
|
530
|
-
bio = typer.prompt("Bio (one-line intro about you)", default="")
|
|
531
618
|
|
|
619
|
+
def _write_config(p, backend, model, api_base, api_key_env, api_key, ns):
|
|
620
|
+
"""Write config.yaml from provider selection."""
|
|
621
|
+
import yaml
|
|
532
622
|
install_source = "local" if (Path.cwd() / ".lorekeep").exists() else "pypi"
|
|
533
|
-
|
|
534
623
|
config = {
|
|
535
624
|
"provider": {
|
|
536
|
-
"backend":
|
|
625
|
+
"backend": backend,
|
|
537
626
|
"model": model,
|
|
538
|
-
"api_base":
|
|
539
|
-
"api_key_env":
|
|
627
|
+
"api_base": api_base,
|
|
628
|
+
"api_key_env": api_key_env,
|
|
540
629
|
"api_key": api_key,
|
|
541
630
|
"temperature": 0.0,
|
|
542
631
|
},
|
|
@@ -545,7 +634,6 @@ def _interactive_init(p: dict) -> tuple[str, str, str]:
|
|
|
545
634
|
"install_source": install_source,
|
|
546
635
|
}
|
|
547
636
|
p["config"].write_text(yaml.dump(config, default_flow_style=False, sort_keys=False))
|
|
548
|
-
return ns, name, bio
|
|
549
637
|
|
|
550
638
|
|
|
551
639
|
def _auto_wire_agents(p: dict, ns: str) -> None:
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Provider/model enumeration via litellm for interactive setup.
|
|
2
|
+
|
|
3
|
+
Lists all providers with chat models, lets the user search and pick,
|
|
4
|
+
then lists chat models for the selected provider with cost info.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ModelInfo:
|
|
15
|
+
"""Display info for a single model."""
|
|
16
|
+
model: str
|
|
17
|
+
provider: str
|
|
18
|
+
mode: str
|
|
19
|
+
input_cost: float # per token
|
|
20
|
+
output_cost: float
|
|
21
|
+
max_input_tokens: int | None
|
|
22
|
+
supports_function_calling: bool
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _suppress_stderr():
|
|
26
|
+
"""Redirect stderr to suppress litellm's 'Provider List' warnings."""
|
|
27
|
+
old = os.dup(2)
|
|
28
|
+
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
29
|
+
os.dup2(devnull, 2)
|
|
30
|
+
return old, devnull
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _restore_stderr(old, devnull):
|
|
34
|
+
os.dup2(old, 2)
|
|
35
|
+
os.close(old)
|
|
36
|
+
os.close(devnull)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def list_providers() -> list[tuple[str, int]]:
|
|
40
|
+
"""Return (provider_name, chat_model_count) for all providers with chat models.
|
|
41
|
+
|
|
42
|
+
Sorted alphabetically.
|
|
43
|
+
"""
|
|
44
|
+
import litellm
|
|
45
|
+
|
|
46
|
+
old, devnull = _suppress_stderr()
|
|
47
|
+
try:
|
|
48
|
+
result: list[tuple[str, int]] = []
|
|
49
|
+
for provider in sorted(litellm.models_by_provider.keys()):
|
|
50
|
+
models = litellm.models_by_provider[provider]
|
|
51
|
+
chat_count = 0
|
|
52
|
+
for m in models:
|
|
53
|
+
try:
|
|
54
|
+
info = litellm.get_model_info(m)
|
|
55
|
+
if info.get("mode") == "chat":
|
|
56
|
+
chat_count += 1
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
if chat_count > 0:
|
|
60
|
+
result.append((provider, chat_count))
|
|
61
|
+
return result
|
|
62
|
+
finally:
|
|
63
|
+
_restore_stderr(old, devnull)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def list_models(provider: str) -> list[ModelInfo]:
|
|
67
|
+
"""Return chat models for a provider, sorted by cost (cheapest first)."""
|
|
68
|
+
import litellm
|
|
69
|
+
|
|
70
|
+
old, devnull = _suppress_stderr()
|
|
71
|
+
try:
|
|
72
|
+
raw = litellm.models_by_provider.get(provider, set())
|
|
73
|
+
models: list[ModelInfo] = []
|
|
74
|
+
for m in sorted(raw):
|
|
75
|
+
try:
|
|
76
|
+
info = litellm.get_model_info(m)
|
|
77
|
+
except Exception:
|
|
78
|
+
continue
|
|
79
|
+
if info.get("mode") != "chat":
|
|
80
|
+
continue
|
|
81
|
+
models.append(ModelInfo(
|
|
82
|
+
model=m,
|
|
83
|
+
provider=provider,
|
|
84
|
+
mode=info.get("mode", "chat"),
|
|
85
|
+
input_cost=info.get("input_cost_per_token", 0) or 0,
|
|
86
|
+
output_cost=info.get("output_cost_per_token", 0) or 0,
|
|
87
|
+
max_input_tokens=info.get("max_input_tokens"),
|
|
88
|
+
supports_function_calling=info.get("supports_function_calling", False),
|
|
89
|
+
))
|
|
90
|
+
models.sort(key=lambda x: x.input_cost)
|
|
91
|
+
return models
|
|
92
|
+
finally:
|
|
93
|
+
_restore_stderr(old, devnull)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def format_cost(cost_per_token: float) -> str:
|
|
97
|
+
"""Format per-token cost as $X/M-tokens."""
|
|
98
|
+
per_million = cost_per_token * 1_000_000
|
|
99
|
+
if per_million == 0:
|
|
100
|
+
return "free"
|
|
101
|
+
if per_million < 1:
|
|
102
|
+
return f"${per_million:.2f}/M"
|
|
103
|
+
return f"${per_million:.0f}/M"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Providers that allow free-text model names (local runtimes)
|
|
107
|
+
DYNAMIC_PROVIDERS = {"ollama", "vllm", "lm_studio", "openai_compat", "aleph_alpha"}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_dynamic(provider: str) -> bool:
|
|
111
|
+
"""True for providers where model names are free-text (ollama, vllm, etc.)."""
|
|
112
|
+
return provider in DYNAMIC_PROVIDERS
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def search_providers(query: str, providers: list[tuple[str, int]] | None = None) -> list[tuple[str, int]]:
|
|
116
|
+
"""Fuzzy search providers by name."""
|
|
117
|
+
if providers is None:
|
|
118
|
+
providers = list_providers()
|
|
119
|
+
q = query.lower()
|
|
120
|
+
return [(p, c) for p, c in providers if q in p.lower()]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Popular providers shown first in the default list
|
|
124
|
+
POPULAR = [
|
|
125
|
+
"openai", "anthropic", "deepseek", "dashscope", "gemini",
|
|
126
|
+
"groq", "mistral", "xai", "ollama", "together_ai",
|
|
127
|
+
"fireworks_ai", "openrouter", "perplexity", "cohere", "ai21",
|
|
128
|
+
]
|
|
@@ -82,12 +82,15 @@ def test_backup_never_tracks_secret_or_regenerable(tmp_path: Path):
|
|
|
82
82
|
(home / "graph").mkdir()
|
|
83
83
|
(home / "graph" / "facts.jsonl").write_text("{}")
|
|
84
84
|
(home / "graph" / "manifest.json").write_text("{}")
|
|
85
|
+
(home / "wiki").mkdir()
|
|
86
|
+
(home / "wiki" / "index.md").write_text("# secret in wiki")
|
|
85
87
|
(home / "cache.json").write_text("{}")
|
|
86
88
|
backup(home)
|
|
87
89
|
tracked = _tracked(home)
|
|
88
90
|
assert "config.yaml" not in tracked
|
|
89
91
|
assert "graph/facts.jsonl" not in tracked
|
|
90
92
|
assert "graph/manifest.json" not in tracked
|
|
93
|
+
assert "wiki/index.md" not in tracked
|
|
91
94
|
assert "cache.json" not in tracked
|
|
92
95
|
|
|
93
96
|
|
|
@@ -43,18 +43,25 @@ def test_init_yes_flag_skips_prompts(tmp_path: Path, monkeypatch):
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def test_init_interactive(tmp_path: Path, monkeypatch):
|
|
46
|
-
"""Interactive:
|
|
46
|
+
"""Interactive: deepseek (3), default model, empty key + env var, ns, name, bio."""
|
|
47
47
|
home = tmp_path / "home"
|
|
48
48
|
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
49
49
|
monkeypatch.setattr("lorekeep.cli._is_interactive", lambda: True)
|
|
50
50
|
|
|
51
|
-
#
|
|
52
|
-
|
|
51
|
+
# Mock list_models to avoid slow litellm calls
|
|
52
|
+
from lorekeep.providers import ModelInfo
|
|
53
|
+
monkeypatch.setattr(
|
|
54
|
+
"lorekeep.providers.list_models",
|
|
55
|
+
lambda p: [ModelInfo("deepseek-chat", p, "chat", 0.28e-6, 0.42e-6, 131000, True)]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# provider=3 (deepseek), model=1, key=empty, env=DEEPSEEK_API_KEY, ns=myteam, name, bio
|
|
59
|
+
result = runner.invoke(app, ["init"], input="3\n1\n\n\nmyteam\nAlice\nBuilds backend infra\n")
|
|
53
60
|
assert result.exit_code == 0, result.stdout
|
|
54
61
|
cfg = yaml.safe_load((home / "config.yaml").read_text())
|
|
55
|
-
assert cfg["provider"]["model"] == "
|
|
62
|
+
assert cfg["provider"]["model"] == "deepseek-chat"
|
|
56
63
|
assert cfg["provider"]["api_key"] is None
|
|
57
|
-
assert cfg["provider"]["api_key_env"]
|
|
64
|
+
assert cfg["provider"]["api_key_env"] == "DEEPSEEK_API_KEY"
|
|
58
65
|
assert cfg["ns"]["default"] == ["myteam"]
|
|
59
66
|
about = home / "raw" / "myteam" / "about.md"
|
|
60
67
|
assert about.exists()
|
|
@@ -64,15 +71,22 @@ def test_init_interactive(tmp_path: Path, monkeypatch):
|
|
|
64
71
|
|
|
65
72
|
|
|
66
73
|
def test_init_interactive_stores_inline_key(tmp_path: Path, monkeypatch):
|
|
67
|
-
"""Interactive: OpenAI
|
|
74
|
+
"""Interactive: OpenAI (1) with inline key stored in config.yaml."""
|
|
68
75
|
home = tmp_path / "home"
|
|
69
76
|
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
70
77
|
monkeypatch.setattr("lorekeep.cli._is_interactive", lambda: True)
|
|
71
78
|
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
from lorekeep.providers import ModelInfo
|
|
80
|
+
monkeypatch.setattr(
|
|
81
|
+
"lorekeep.providers.list_models",
|
|
82
|
+
lambda p: [ModelInfo("gpt-4o-mini", p, "chat", 0.15e-6, 0.6e-6, 128000, True)]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# provider=1 (openai), model=1, key=sk-testKEY, ns=me, name, bio
|
|
86
|
+
result = runner.invoke(app, ["init"], input="1\n1\nsk-testKEY\nme\nBob\nlocal dev\n")
|
|
74
87
|
assert result.exit_code == 0, result.stdout
|
|
75
88
|
cfg = yaml.safe_load((home / "config.yaml").read_text())
|
|
89
|
+
assert cfg["provider"]["model"] == "gpt-4o-mini"
|
|
76
90
|
assert cfg["provider"]["api_key"] == "sk-testKEY"
|
|
77
91
|
assert cfg["provider"]["api_key_env"] is None
|
|
78
92
|
assert cfg["ns"]["default"] == ["me"]
|
|
@@ -82,16 +96,26 @@ def test_init_interactive_stores_inline_key(tmp_path: Path, monkeypatch):
|
|
|
82
96
|
|
|
83
97
|
|
|
84
98
|
def test_init_interactive_ollama_no_key(tmp_path: Path, monkeypatch):
|
|
85
|
-
"""Ollama
|
|
99
|
+
"""Ollama: free-text model, api_base prompt, no API key needed."""
|
|
86
100
|
home = tmp_path / "home"
|
|
87
101
|
monkeypatch.setenv("LOREKEEP_HOME", str(home))
|
|
88
102
|
monkeypatch.setattr("lorekeep.cli._is_interactive", lambda: True)
|
|
89
103
|
|
|
90
|
-
# Ollama
|
|
91
|
-
|
|
104
|
+
# Ollama is now in POPULAR list but is dynamic — free-text model + api_base
|
|
105
|
+
# Find ollama's index in POPULAR list
|
|
106
|
+
from lorekeep.providers import POPULAR
|
|
107
|
+
ollama_idx = POPULAR.index("ollama") + 1 if "ollama" in POPULAR else None
|
|
108
|
+
|
|
109
|
+
if ollama_idx is None:
|
|
110
|
+
# ollama not in POPULAR, use search
|
|
111
|
+
pytest.skip("ollama not in POPULAR list")
|
|
112
|
+
|
|
113
|
+
# provider=ollama_idx, model=llama3.2, api_base=default, ns=myproject, name, bio
|
|
114
|
+
inp = f"{ollama_idx}\nllama3.2\n\nmyproject\nCJ\ndemo\n"
|
|
115
|
+
result = runner.invoke(app, ["init"], input=inp)
|
|
92
116
|
assert result.exit_code == 0, result.stdout
|
|
93
117
|
cfg = yaml.safe_load((home / "config.yaml").read_text())
|
|
94
|
-
assert cfg["provider"]["model"] == "
|
|
118
|
+
assert cfg["provider"]["model"] == "llama3.2"
|
|
95
119
|
assert cfg["provider"]["api_key"] is None
|
|
96
120
|
assert cfg["ns"]["default"] == ["myproject"]
|
|
97
121
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Tests for provider/model enumeration."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from lorekeep.providers import (
|
|
9
|
+
ModelInfo,
|
|
10
|
+
format_cost,
|
|
11
|
+
is_dynamic,
|
|
12
|
+
search_providers,
|
|
13
|
+
DYNAMIC_PROVIDERS,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestFormatCost:
|
|
18
|
+
def test_free(self):
|
|
19
|
+
assert format_cost(0) == "free"
|
|
20
|
+
|
|
21
|
+
def test_sub_dollar(self):
|
|
22
|
+
assert format_cost(0.00000028) == "$0.28/M"
|
|
23
|
+
|
|
24
|
+
def test_above_dollar(self):
|
|
25
|
+
assert format_cost(0.000003) == "$3/M"
|
|
26
|
+
|
|
27
|
+
def test_high_cost(self):
|
|
28
|
+
assert format_cost(0.000015) == "$15/M"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestIsDynamic:
|
|
32
|
+
def test_ollama(self):
|
|
33
|
+
assert is_dynamic("ollama")
|
|
34
|
+
|
|
35
|
+
def test_vllm(self):
|
|
36
|
+
assert is_dynamic("vllm")
|
|
37
|
+
|
|
38
|
+
def test_openai_not_dynamic(self):
|
|
39
|
+
assert not is_dynamic("openai")
|
|
40
|
+
|
|
41
|
+
def test_deepseek_not_dynamic(self):
|
|
42
|
+
assert not is_dynamic("deepseek")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestSearchProviders:
|
|
46
|
+
def test_search_deepseek(self):
|
|
47
|
+
providers = [("openai", 150), ("deepseek", 4), ("anthropic", 22)]
|
|
48
|
+
results = search_providers("deep", providers)
|
|
49
|
+
assert results == [("deepseek", 4)]
|
|
50
|
+
|
|
51
|
+
def test_search_no_match(self):
|
|
52
|
+
providers = [("openai", 150), ("anthropic", 22)]
|
|
53
|
+
results = search_providers("nonexistent", providers)
|
|
54
|
+
assert results == []
|
|
55
|
+
|
|
56
|
+
def test_search_partial(self):
|
|
57
|
+
providers = [("openai", 150), ("openrouter", 96), ("anthropic", 22)]
|
|
58
|
+
results = search_providers("open", providers)
|
|
59
|
+
assert len(results) == 2
|
|
60
|
+
assert "openai" in [r[0] for r in results]
|
|
61
|
+
assert "openrouter" in [r[0] for r in results]
|
|
62
|
+
|
|
63
|
+
def test_search_case_insensitive(self):
|
|
64
|
+
providers = [("OpenAI", 150)]
|
|
65
|
+
results = search_providers("open", providers)
|
|
66
|
+
assert len(results) == 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestListProviders:
|
|
70
|
+
"""Tests that mock litellm to avoid network/import issues."""
|
|
71
|
+
|
|
72
|
+
@patch("litellm.models_by_provider")
|
|
73
|
+
@patch("litellm.get_model_info")
|
|
74
|
+
def test_returns_providers_with_chat_models(self, mock_info, mock_map):
|
|
75
|
+
from lorekeep.providers import list_providers
|
|
76
|
+
|
|
77
|
+
mock_map.keys.return_value = ["openai", "anthropic", "assemblyai"]
|
|
78
|
+
mock_map.__getitem__ = MagicMock(side_effect=lambda k: {
|
|
79
|
+
"openai": {"gpt-4o"},
|
|
80
|
+
"anthropic": {"claude-3-sonnet"},
|
|
81
|
+
"assemblyai": set(),
|
|
82
|
+
}[k])
|
|
83
|
+
|
|
84
|
+
def fake_info(model):
|
|
85
|
+
if "gpt" in model:
|
|
86
|
+
return {"mode": "chat"}
|
|
87
|
+
if "claude" in model:
|
|
88
|
+
return {"mode": "chat"}
|
|
89
|
+
return {"mode": "audio_transcription"}
|
|
90
|
+
|
|
91
|
+
mock_info.side_effect = fake_info
|
|
92
|
+
|
|
93
|
+
result = list_providers()
|
|
94
|
+
names = [r[0] for r in result]
|
|
95
|
+
assert "openai" in names
|
|
96
|
+
assert "anthropic" in names
|
|
97
|
+
assert "assemblyai" not in names
|
|
98
|
+
|
|
99
|
+
@patch("litellm.models_by_provider")
|
|
100
|
+
@patch("litellm.get_model_info")
|
|
101
|
+
def test_counts_chat_models(self, mock_info, mock_map):
|
|
102
|
+
from lorekeep.providers import list_providers
|
|
103
|
+
|
|
104
|
+
mock_map.keys.return_value = ["openai"]
|
|
105
|
+
mock_map.__getitem__ = MagicMock(return_value={"gpt-4o", "text-embedding-3"})
|
|
106
|
+
|
|
107
|
+
def fake_info(model):
|
|
108
|
+
return {"mode": "chat"} if "gpt" in model else {"mode": "embedding"}
|
|
109
|
+
|
|
110
|
+
mock_info.side_effect = fake_info
|
|
111
|
+
|
|
112
|
+
result = list_providers()
|
|
113
|
+
assert result == [("openai", 1)]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestListModels:
|
|
117
|
+
@patch("litellm.models_by_provider", {"openai": {"model-a", "model-b", "model-c"}})
|
|
118
|
+
@patch("litellm.get_model_info")
|
|
119
|
+
def test_returns_chat_models_sorted_by_cost(self, mock_info):
|
|
120
|
+
from lorekeep.providers import list_models
|
|
121
|
+
|
|
122
|
+
models_data = {
|
|
123
|
+
"model-a": {"mode": "chat", "input_cost_per_token": 0.000003, "output_cost_per_token": 0.000015,
|
|
124
|
+
"max_input_tokens": 128000, "supports_function_calling": True},
|
|
125
|
+
"model-b": {"mode": "chat", "input_cost_per_token": 0.0000005, "output_cost_per_token": 0.000001,
|
|
126
|
+
"max_input_tokens": 32000, "supports_function_calling": False},
|
|
127
|
+
"model-c": {"mode": "embedding", "input_cost_per_token": 0.0, "output_cost_per_token": 0.0,
|
|
128
|
+
"max_input_tokens": None, "supports_function_calling": False},
|
|
129
|
+
}
|
|
130
|
+
mock_info.side_effect = lambda m: models_data[m]
|
|
131
|
+
|
|
132
|
+
result = list_models("openai")
|
|
133
|
+
assert len(result) == 2
|
|
134
|
+
assert result[0].model == "model-b" # cheaper first
|
|
135
|
+
assert result[1].model == "model-a"
|
|
136
|
+
assert all(r.mode == "chat" for r in result)
|
|
137
|
+
|
|
138
|
+
@patch("litellm.models_by_provider", {"openai": {"good", "bad"}})
|
|
139
|
+
@patch("litellm.get_model_info")
|
|
140
|
+
def test_skips_unmapped_models(self, mock_info):
|
|
141
|
+
from lorekeep.providers import list_models
|
|
142
|
+
|
|
143
|
+
def fake_info(model):
|
|
144
|
+
if model == "bad":
|
|
145
|
+
raise ValueError("unmapped")
|
|
146
|
+
return {"mode": "chat", "input_cost_per_token": 0, "output_cost_per_token": 0,
|
|
147
|
+
"max_input_tokens": None, "supports_function_calling": False}
|
|
148
|
+
|
|
149
|
+
mock_info.side_effect = fake_info
|
|
150
|
+
|
|
151
|
+
result = list_models("openai")
|
|
152
|
+
assert len(result) == 1
|
|
153
|
+
assert result[0].model == "good"
|
|
154
|
+
|
|
155
|
+
@patch("litellm.models_by_provider", {})
|
|
156
|
+
def test_empty_provider(self):
|
|
157
|
+
from lorekeep.providers import list_models
|
|
158
|
+
result = list_models("nonexistent")
|
|
159
|
+
assert result == []
|
|
@@ -1024,7 +1024,7 @@ wheels = [
|
|
|
1024
1024
|
|
|
1025
1025
|
[[package]]
|
|
1026
1026
|
name = "lorekeep"
|
|
1027
|
-
version = "0.8.
|
|
1027
|
+
version = "0.8.4"
|
|
1028
1028
|
source = { editable = "." }
|
|
1029
1029
|
dependencies = [
|
|
1030
1030
|
{ name = "litellm" },
|
|
@@ -1630,16 +1630,16 @@ wheels = [
|
|
|
1630
1630
|
|
|
1631
1631
|
[[package]]
|
|
1632
1632
|
name = "pydantic-settings"
|
|
1633
|
-
version = "2.14.
|
|
1633
|
+
version = "2.14.2"
|
|
1634
1634
|
source = { registry = "https://pypi.org/simple" }
|
|
1635
1635
|
dependencies = [
|
|
1636
1636
|
{ name = "pydantic" },
|
|
1637
1637
|
{ name = "python-dotenv" },
|
|
1638
1638
|
{ name = "typing-inspection" },
|
|
1639
1639
|
]
|
|
1640
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1640
|
+
sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" }
|
|
1641
1641
|
wheels = [
|
|
1642
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1642
|
+
{ url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" },
|
|
1643
1643
|
]
|
|
1644
1644
|
|
|
1645
1645
|
[[package]]
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from lorekeep.compile.providers import LLMProvider, FakeProvider, LiteLLMProvider
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def test_fake_provider_returns_fixed_output():
|
|
5
|
-
p = FakeProvider(responses=['{"nodes":[],"edges":[]}'])
|
|
6
|
-
assert p.extract_json("sys", "user") == '{"nodes":[],"edges":[]}'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def test_fake_provider_raises_when_empty():
|
|
10
|
-
p = FakeProvider(responses=[])
|
|
11
|
-
try:
|
|
12
|
-
p.extract_json("sys", "user")
|
|
13
|
-
assert False, "expected RuntimeError"
|
|
14
|
-
except RuntimeError:
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_litellm_provider_holds_config():
|
|
19
|
-
p = LiteLLMProvider(model="ollama/llama3", api_base="http://localhost:11434")
|
|
20
|
-
assert p.model == "ollama/llama3"
|
|
21
|
-
assert p.api_base == "http://localhost:11434"
|
|
22
|
-
assert isinstance(p, LLMProvider)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lorekeep-0.8.4 → lorekeep-0.9.0}/docs/superpowers/plans/2026-06-26-lorekeep-dotdir-backup.md
RENAMED
|
File without changes
|
{lorekeep-0.8.4 → lorekeep-0.9.0}/docs/superpowers/specs/2026-06-26-lorekeep-dotdir-layout-design.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lorekeep-0.8.4 → lorekeep-0.9.0}/tests/fixtures/claude-session/memory/lorekeep-project-state.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|