lcode-cli 0.35.1__tar.gz → 0.36.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.
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/CHANGELOG.md +13 -1
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/PKG-INFO +1 -1
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/models.md +14 -1
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/quickstart.md +1 -1
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/usage.md +1 -1
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/__init__.py +1 -1
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/cli.py +81 -21
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/repl.py +80 -31
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_backends.py +14 -0
- lcode_cli-0.36.0/tests/test_model_picker.py +94 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_residency.py +2 -2
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.editorconfig +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.github/CODEOWNERS +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.github/dependabot.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.github/workflows/ci.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.github/workflows/docs.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.github/workflows/release.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.gitignore +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/.pre-commit-config.yaml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/AGENTS.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/CODE_OF_CONDUCT.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/CONTRIBUTING.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/LICENSE +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/README.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/SECURITY.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/action.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/agents.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/assets/demo.svg +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/assets/extra.css +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/automation.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/bench.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/changelog.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/commands.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/configuration.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/contributing.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/editors.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/git.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/hooks.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/how-it-works.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/index.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/installation.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/lsp.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/mcp.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/memory.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/sandbox.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/search.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/servers.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/docs/troubleshooting.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/examples/README.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/examples/api/fix_tests.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/examples/commands/review.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/examples/skills/commit-message/SKILL.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/install.sh +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/mkdocs.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/overrides/main.html +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/pyproject.toml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/scripts/make_logos.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/scripts/record_demo.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/scripts/social-preview.html +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/__main__.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/acp.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/action.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/agent.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/api.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/backends.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/bench.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/bench_backend.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/bench_runtime.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/catalog.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/checkpoints.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/checks.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/clipboard.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/codesearch.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/config.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/context.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/escalate.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/extensions.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/frontmatter.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/gitflow.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/hardware.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/hooks.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/jobs.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/limits.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/lsp.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/matching.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/mcp/__init__.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/mcp/auth.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/mcp/catalog.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/mcp/catalog.toml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/mcp/client.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/mcp/commands.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/mcp/config.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/mcp/http.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/mcp/manager.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/mcp/protocol.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/mcp/server.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/mcp/stdio.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/memory.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/models.toml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/notify.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/ollama.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/permissions.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/planning.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/render.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/repeats.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/repomap.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/residency.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/review.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/sandbox.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/screenshots.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/secretscan.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/servers/__init__.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/servers/encord.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/servers/valohai.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/servers/xai.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/sessions.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/steer.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/subagents.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/tools.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/unreachable.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/update.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/vision.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/src/lcode/web.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/conftest.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/fake_lsp_server.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/fake_mcp_server.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_acp.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_action.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_agent.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_api.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_bench.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_catalog.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_checkpoints.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_checks.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_cli.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_clipboard.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_codesearch.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_config.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_context.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_context_picker.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_escalate.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_extensions.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_gitflow.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_hooks.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_jobs.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_lsp.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_matching.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_mcp.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_memory.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_permissions.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_plan_mode.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_repeats.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_repomap.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_review.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_robustness.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_sandbox.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_screenshots.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_secretscan.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_servers.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_sessions.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_steer.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_stuck.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_subagents.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_tools.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_update.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_vision.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_web.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/tests/test_xai.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.36.0}/uv.lock +0 -0
|
@@ -6,6 +6,17 @@ All notable changes to lcode are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.36.0] - 2026-10-18
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `/models` and `/model` show a numbered list of models, and you switch by typing a number: no
|
|
14
|
+
second `/model <name>`. `/model 2` switches straight away, and a name still works. The list ends
|
|
15
|
+
with the other Ollama models you've installed that can call tools, and a model that isn't
|
|
16
|
+
installed says how to get it.
|
|
17
|
+
- `/context` says why it stops at the model's largest window (256K for qwen3.6-35b): Ollama caps the
|
|
18
|
+
window at what the model was trained on, whatever the memory, so 512K or 1M would only look larger.
|
|
19
|
+
|
|
9
20
|
## [0.35.1] - 2026-10-15
|
|
10
21
|
|
|
11
22
|
### Fixed
|
|
@@ -799,7 +810,8 @@ First public release.
|
|
|
799
810
|
- `AGENTS.md` project instructions and `/init` to generate them.
|
|
800
811
|
- One-line installer for Linux and macOS.
|
|
801
812
|
|
|
802
|
-
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.
|
|
813
|
+
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.36.0...HEAD
|
|
814
|
+
[0.36.0]: https://github.com/nasser1941/lcode/compare/v0.35.1...v0.36.0
|
|
803
815
|
[0.35.1]: https://github.com/nasser1941/lcode/compare/v0.35.0...v0.35.1
|
|
804
816
|
[0.35.0]: https://github.com/nasser1941/lcode/compare/v0.34.0...v0.35.0
|
|
805
817
|
[0.34.0]: https://github.com/nasser1941/lcode/compare/v0.33.1...v0.34.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lcode-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.36.0
|
|
4
4
|
Summary: A local-first terminal coding agent powered by open-weight models on your own GPU or Mac, via Ollama.
|
|
5
5
|
Project-URL: Homepage, https://nasser1941.github.io/lcode/
|
|
6
6
|
Project-URL: Documentation, https://nasser1941.github.io/lcode/
|
|
@@ -60,9 +60,18 @@ when they fit entirely in GPU (or Apple unified) memory.
|
|
|
60
60
|
|
|
61
61
|
```text
|
|
62
62
|
❯ /models
|
|
63
|
-
|
|
63
|
+
Models for this machine: RTX 4080 Laptop (12 GB VRAM), 31 GB RAM
|
|
64
|
+
# Key Size Max ctx Fits here Speed Status
|
|
65
|
+
1 qwen3.6-35b 22.6 GB 256K 256K good (experts in RAM) installed, recommended, current
|
|
66
|
+
2 qwen3.8-27b …
|
|
67
|
+
9 my-model:7b installed, not in catalog
|
|
68
|
+
Switch to which model? (number or name, Enter to keep lcode-qwen3.6-35b): 9
|
|
64
69
|
```
|
|
65
70
|
|
|
71
|
+
`/model` shows the same list. `/model 9` or `/model qwen3.5-9b` switches straight away. The
|
|
72
|
+
list ends with the other Ollama models you've installed that can call tools. A model that isn't
|
|
73
|
+
installed yet says how to get it (`lcode setup <key>` in a shell).
|
|
74
|
+
|
|
66
75
|
### Any other Ollama model
|
|
67
76
|
|
|
68
77
|
```bash
|
|
@@ -107,6 +116,10 @@ Context window for lcode-qwen3.6-35b
|
|
|
107
116
|
Choose a number, a size like 96k, or press Enter to keep 256K: 2
|
|
108
117
|
```
|
|
109
118
|
|
|
119
|
+
The list stops at the largest window the model was trained on, and says so: Ollama caps the window
|
|
120
|
+
there whatever the memory, so a larger setting would only look larger. For qwen3.6-35b that's 256K;
|
|
121
|
+
512K and 1M are offered for models trained on them.
|
|
122
|
+
|
|
110
123
|
Sizes accept `k` and `m` suffixes (`128k` = 131,072 tokens, `1m` = 1,048,576). When the window is
|
|
111
124
|
85% full, lcode summarizes the conversation automatically so you can keep working; `/compact` does it
|
|
112
125
|
on demand.
|
|
@@ -76,7 +76,7 @@ reasoning is on.
|
|
|
76
76
|
| `/plan [request]` | Work out a [plan](#plan-mode) before changing anything, or show the approved plan |
|
|
77
77
|
| `/compact [focus]` | Summarize the conversation to free context |
|
|
78
78
|
| `/context [size]` | Show how full the context window is, what's using it (by category), and change its size: pick from a list with memory estimates, or give a size like `/context 128k` (`/ctx` is a shortcut) |
|
|
79
|
-
| `/model [name]
|
|
79
|
+
| `/models`, `/model [number or name]` | List the models and how they fit this machine, numbered: type a number to switch, or `/model 2`, `/model qwen3.5-9b` |
|
|
80
80
|
| `/think [on\|off]` | Toggle reasoning |
|
|
81
81
|
| `/verbose` | Toggle showing the reasoning text |
|
|
82
82
|
| `/mode [ask\|plan\|auto-edit\|yolo]` | Set the permission mode |
|
|
@@ -113,44 +113,96 @@ def choose_context(
|
|
|
113
113
|
# ----------------------------------------------------------------------------- lcode models
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
def print_served(
|
|
116
|
+
def print_served(
|
|
117
|
+
client: backends.OpenAICompatible, current: str | None, numbered: bool = False, out: Console | None = None
|
|
118
|
+
) -> list[str]:
|
|
119
|
+
out = out or console
|
|
117
120
|
try:
|
|
118
121
|
served = client.chat_models()
|
|
119
122
|
current = backends.pick_model(client, current) if current else None
|
|
120
123
|
except OllamaError as e:
|
|
121
124
|
if not isinstance(e, backends.BackendError) or "doesn't serve" not in str(e):
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
out.print(f"[red]{e}[/]")
|
|
126
|
+
return []
|
|
127
|
+
served = []
|
|
128
|
+
out.print(f"[bold]Models served by {escape(client.describe())}[/]")
|
|
129
|
+
for i, name in enumerate(served, 1):
|
|
130
|
+
number = f"[cyan]{i:>3}[/] " if numbered else " "
|
|
131
|
+
out.print(f"{number}{escape(name)}" + (" [bold]current[/]" if name == current else ""))
|
|
127
132
|
if not served:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
out.print(" none listed")
|
|
134
|
+
if not numbered:
|
|
135
|
+
out.print(
|
|
136
|
+
"[dim]Models are downloaded and loaded in the server itself. Choose one with [/]lcode --model <id>[dim] "
|
|
137
|
+
"or [/]lcode config set model <id>[dim].[/]"
|
|
138
|
+
)
|
|
139
|
+
return served
|
|
133
140
|
|
|
134
141
|
|
|
135
|
-
def
|
|
142
|
+
def other_installed(ollama: Ollama, installed: set[str]) -> list[str]:
|
|
143
|
+
"""Installed Ollama models outside the catalog that can chat and call tools (so lcode can use them), by name.
|
|
144
|
+
Embedding models are left out: Ollama may list `tools` for them too, but not `completion`."""
|
|
145
|
+
known = {name for spec in catalog.load() for name in (spec.tag, spec.local_name)}
|
|
146
|
+
usable = []
|
|
147
|
+
for name in sorted({n.removesuffix(":latest") for n in installed} - known):
|
|
148
|
+
try:
|
|
149
|
+
capabilities = ollama.show(name).get("capabilities") or []
|
|
150
|
+
if "completion" in capabilities and "tools" in capabilities:
|
|
151
|
+
usable.append(name)
|
|
152
|
+
except OllamaError:
|
|
153
|
+
continue
|
|
154
|
+
return usable
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def model_choices(ollama: Ollama | None) -> list[str]:
|
|
158
|
+
"""What `/models` numbers, in its order: the catalog's keys, then other installed models that call tools.
|
|
159
|
+
For an OpenAI-compatible server, the models it serves."""
|
|
136
160
|
if ollama is not None and backends.is_openai(ollama):
|
|
137
|
-
|
|
138
|
-
|
|
161
|
+
try:
|
|
162
|
+
return ollama.chat_models() # type: ignore[union-attr]
|
|
163
|
+
except OllamaError:
|
|
164
|
+
return []
|
|
165
|
+
try:
|
|
166
|
+
installed = ollama.installed_names() if ollama else set()
|
|
167
|
+
except OllamaError:
|
|
168
|
+
installed = set()
|
|
169
|
+
return [spec.key for spec in catalog.load()] + (other_installed(ollama, installed) if ollama else [])
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def print_models(
|
|
173
|
+
ollama: Ollama | None,
|
|
174
|
+
hw: Hardware,
|
|
175
|
+
current: str | None = None,
|
|
176
|
+
numbered: bool = False,
|
|
177
|
+
out: Console | None = None,
|
|
178
|
+
) -> list[str]:
|
|
179
|
+
"""The models table; with `numbered`, a number on each row to pick it by. Returns the rows' names in order."""
|
|
180
|
+
out = out or console
|
|
181
|
+
if ollama is not None and backends.is_openai(ollama):
|
|
182
|
+
return print_served(ollama, current, numbered, out) # type: ignore[arg-type]
|
|
139
183
|
try:
|
|
140
184
|
installed = ollama.installed_names() if ollama else set()
|
|
141
185
|
except OllamaError:
|
|
142
186
|
installed = set()
|
|
143
187
|
rec = catalog.recommend(hw)
|
|
144
|
-
wide =
|
|
188
|
+
wide = out.width >= 130 # the descriptive columns don't fit an 80-column terminal
|
|
145
189
|
columns = ["Key", "Model", "Type", "Size", "Max ctx", "Fits here", "Speed", "Status"]
|
|
146
190
|
if not wide:
|
|
147
191
|
columns = [c for c in columns if c not in ("Model", "Type")]
|
|
192
|
+
if numbered:
|
|
193
|
+
columns.insert(0, "#")
|
|
148
194
|
table = Table(title=f"Models for this machine: {hw.describe()}", title_justify="left", header_style="bold")
|
|
149
195
|
for col in columns:
|
|
150
196
|
table.add_column(
|
|
151
|
-
col,
|
|
197
|
+
col,
|
|
198
|
+
no_wrap=col in ("#", "Key", "Size", "Max ctx", "Fits here"),
|
|
199
|
+
min_width=11 if col == "Status" else None,
|
|
200
|
+
justify="right" if col == "#" else "left",
|
|
201
|
+
style="cyan" if col == "#" else None,
|
|
152
202
|
)
|
|
203
|
+
choices: list[str] = []
|
|
153
204
|
for spec in catalog.load():
|
|
205
|
+
choices.append(spec.key)
|
|
154
206
|
ctx, speed = spec.fit(hw)
|
|
155
207
|
marks = []
|
|
156
208
|
if spec.local_name in installed or spec.tag in installed:
|
|
@@ -161,6 +213,7 @@ def print_models(ollama: Ollama | None, hw: Hardware, current: str | None = None
|
|
|
161
213
|
marks.append("[bold]current[/]")
|
|
162
214
|
marks.append("tested" if spec.tested else "[dim]untested[/]")
|
|
163
215
|
row = {
|
|
216
|
+
"#": str(len(choices)),
|
|
164
217
|
"Key": spec.key,
|
|
165
218
|
"Model": spec.name,
|
|
166
219
|
"Type": spec.params,
|
|
@@ -171,11 +224,18 @@ def print_models(ollama: Ollama | None, hw: Hardware, current: str | None = None
|
|
|
171
224
|
"Status": ", ".join(marks) if wide else "\n".join(marks),
|
|
172
225
|
}
|
|
173
226
|
table.add_row(*(row[c] for c in columns))
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"[
|
|
177
|
-
|
|
178
|
-
|
|
227
|
+
for name in other_installed(ollama, installed) if ollama else []:
|
|
228
|
+
choices.append(name)
|
|
229
|
+
marks = ["[green]installed[/]"] + (["[bold]current[/]"] if name == current else []) + ["[dim]not in catalog[/]"]
|
|
230
|
+
row = {"#": str(len(choices)), "Key": escape(name), "Status": ", ".join(marks) if wide else "\n".join(marks)}
|
|
231
|
+
table.add_row(*(row.get(c, "") for c in columns))
|
|
232
|
+
out.print(table)
|
|
233
|
+
if not numbered:
|
|
234
|
+
out.print(
|
|
235
|
+
"[dim]Install one with [/]lcode setup <key>[dim]. 'Fits here' is the largest context that fits in memory; "
|
|
236
|
+
"choose another with --context. Any other Ollama model with tool calling also works: lcode --model <tag>[/]"
|
|
237
|
+
)
|
|
238
|
+
return choices
|
|
179
239
|
|
|
180
240
|
|
|
181
241
|
def cmd_models(args) -> None:
|
|
@@ -68,8 +68,8 @@ COMMANDS = {
|
|
|
68
68
|
"/compact": "Summarize the conversation to free context (optional: what to focus on)",
|
|
69
69
|
"/context": "Show context usage and change the window size: pick from a list, or /context 128k",
|
|
70
70
|
"/ctx": "Shortcut for /context",
|
|
71
|
-
"/model": "
|
|
72
|
-
"/models": "List models and how they fit this machine",
|
|
71
|
+
"/model": "Switch model: pick a number from the list, or /model 2, /model qwen3.5-9b",
|
|
72
|
+
"/models": "List models and how they fit this machine; type a number to switch",
|
|
73
73
|
"/think": "Toggle model reasoning on/off",
|
|
74
74
|
"/verbose": "Toggle showing the model's reasoning text",
|
|
75
75
|
"/plan": "Plan before changing anything: /plan <request>, or /plan to show the approved plan",
|
|
@@ -243,14 +243,20 @@ def banner(agent: Agent) -> None:
|
|
|
243
243
|
CONTEXT_CHOICES = (16384, 32768, 65536, 131072, 262144, 524288, 1048576)
|
|
244
244
|
|
|
245
245
|
|
|
246
|
+
def model_limit(agent: Agent) -> int | None:
|
|
247
|
+
"""The largest context the model supports (what Ollama reports, else the catalog's), if known."""
|
|
248
|
+
spec = catalog.find(agent.settings.model)
|
|
249
|
+
try:
|
|
250
|
+
return agent.ollama.max_context(agent.settings.model) or (spec.max_context if spec else None)
|
|
251
|
+
except OllamaError:
|
|
252
|
+
return spec.max_context if spec else None
|
|
253
|
+
|
|
254
|
+
|
|
246
255
|
def context_options(agent: Agent, hardware: Hardware) -> list[dict]:
|
|
247
256
|
"""The context sizes this model supports, with how each one fits this machine."""
|
|
248
257
|
s = agent.settings
|
|
249
258
|
spec = catalog.find(s.model)
|
|
250
|
-
|
|
251
|
-
max_ctx = agent.ollama.max_context(s.model) or (spec.max_context if spec else None)
|
|
252
|
-
except OllamaError:
|
|
253
|
-
max_ctx = spec.max_context if spec else None
|
|
259
|
+
max_ctx = model_limit(agent)
|
|
254
260
|
sizes = {c for c in CONTEXT_CHOICES if not max_ctx or c <= max_ctx} | {s.context}
|
|
255
261
|
if max_ctx:
|
|
256
262
|
sizes.add(max_ctx)
|
|
@@ -317,6 +323,15 @@ def context_command(agent: Agent, arg: str, hardware: Hardware) -> None:
|
|
|
317
323
|
marks.append("[cyan]recommended[/]")
|
|
318
324
|
table.add_row(str(i), format_tokens(option["size"]), option["note"], ", ".join(marks))
|
|
319
325
|
c.print(table)
|
|
326
|
+
limit = model_limit(agent)
|
|
327
|
+
beyond = [size for size in CONTEXT_CHOICES if limit and size > limit]
|
|
328
|
+
if beyond:
|
|
329
|
+
sizes = " and ".join(format_tokens(size) for size in beyond[:2])
|
|
330
|
+
c.print(
|
|
331
|
+
f"[dim]Not offered: {sizes}{' and more' if len(beyond) > 2 else ''}. {s.model} was trained on up to "
|
|
332
|
+
f"{format_tokens(limit)} tokens, and Ollama caps the window there whatever the memory, so a larger "
|
|
333
|
+
"setting would only look larger.[/]"
|
|
334
|
+
)
|
|
320
335
|
try:
|
|
321
336
|
answer = input(f" Choose a number, a size like 96k, or press Enter to keep {format_tokens(s.context)}: ")
|
|
322
337
|
except EOFError:
|
|
@@ -809,6 +824,63 @@ def restore_checkpoint(agent: Agent, target: Checkpoint, rewind: bool) -> None:
|
|
|
809
824
|
agent.save()
|
|
810
825
|
|
|
811
826
|
|
|
827
|
+
def model_command(agent: Agent, arg: str, hardware: Hardware) -> None:
|
|
828
|
+
"""/models and /model: the numbered list to pick from, or straight to a number or a name (/model 2)."""
|
|
829
|
+
from lcode.cli import model_choices, print_models
|
|
830
|
+
|
|
831
|
+
c, s = agent.console, agent.settings
|
|
832
|
+
if arg and not arg.isdigit():
|
|
833
|
+
switch_model(agent, arg, hardware)
|
|
834
|
+
return
|
|
835
|
+
if arg:
|
|
836
|
+
choices = model_choices(agent.ollama)
|
|
837
|
+
else:
|
|
838
|
+
choices = print_models(agent.ollama, hardware, s.model, numbered=True, out=c)
|
|
839
|
+
if not choices:
|
|
840
|
+
return
|
|
841
|
+
try:
|
|
842
|
+
answer = input(f" Switch to which model? (number or name, Enter to keep {s.model}): ").strip()
|
|
843
|
+
except EOFError:
|
|
844
|
+
return
|
|
845
|
+
if not answer:
|
|
846
|
+
c.print(f"Keeping {s.model}.")
|
|
847
|
+
return
|
|
848
|
+
arg = answer
|
|
849
|
+
if arg.isdigit():
|
|
850
|
+
if not 1 <= int(arg) <= len(choices):
|
|
851
|
+
c.print(f"[red]There's no model {arg} in the list: choose 1 to {len(choices)}.[/]")
|
|
852
|
+
return
|
|
853
|
+
arg = choices[int(arg) - 1]
|
|
854
|
+
switch_model(agent, arg, hardware)
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def switch_model(agent: Agent, wanted: str, hardware: Hardware) -> None:
|
|
858
|
+
"""Switch to a catalog key or an installed model, and size the context for it."""
|
|
859
|
+
from lcode.cli import NotInstalled, resolve_model
|
|
860
|
+
|
|
861
|
+
c, s = agent.console, agent.settings
|
|
862
|
+
try:
|
|
863
|
+
name, spec = resolve_model(agent.ollama, wanted)
|
|
864
|
+
except (NotInstalled, OllamaError) as e:
|
|
865
|
+
c.print(f"[red]{e}[/]")
|
|
866
|
+
return
|
|
867
|
+
if name == s.model:
|
|
868
|
+
c.print(f"Already using {name} (context {format_tokens(s.context)}).")
|
|
869
|
+
return
|
|
870
|
+
s.model = name
|
|
871
|
+
s.num_batch = spec.num_batch if spec and name == spec.local_name else None
|
|
872
|
+
if spec: # size the context for the new model: largest window that fits this machine
|
|
873
|
+
s.context = limits.cap(name, spec.fit(hardware)[0] or MIN_USEFUL_CONTEXT)
|
|
874
|
+
elif backends.is_openai(agent.ollama):
|
|
875
|
+
s.context = backends.context_for(agent.ollama, name, None)[0] # the server's window
|
|
876
|
+
agent._vision = False # the model that looks at images goes with the model: decide again
|
|
877
|
+
agent.messages[0]["content"] = agent.system_prompt()
|
|
878
|
+
c.print(f"[green]Switched to {name}[/] (context {format_tokens(s.context)}).")
|
|
879
|
+
warning = residency.dense_warning(agent.ollama, name, s.context, hardware)
|
|
880
|
+
if warning:
|
|
881
|
+
agent.say(warning, warning=True)
|
|
882
|
+
|
|
883
|
+
|
|
812
884
|
def apply_context(agent: Agent, size: int, hardware: Hardware) -> None:
|
|
813
885
|
c, s = agent.console, agent.settings
|
|
814
886
|
if size == s.context:
|
|
@@ -956,8 +1028,6 @@ def session_start(agent: Agent) -> None:
|
|
|
956
1028
|
|
|
957
1029
|
def handle_command(agent: Agent, line: str, hardware: Hardware) -> bool:
|
|
958
1030
|
"""Run a slash command. Returns False when the REPL should exit."""
|
|
959
|
-
from lcode.cli import NotInstalled, print_models, resolve_model
|
|
960
|
-
|
|
961
1031
|
cmd, _, arg = line.partition(" ")
|
|
962
1032
|
arg = arg.strip()
|
|
963
1033
|
c, s = agent.console, agent.settings
|
|
@@ -1006,29 +1076,8 @@ def handle_command(agent: Agent, line: str, hardware: Hardware) -> bool:
|
|
|
1006
1076
|
agent.compact(arg)
|
|
1007
1077
|
elif cmd in ("/context", "/ctx"):
|
|
1008
1078
|
context_command(agent, arg, hardware)
|
|
1009
|
-
elif cmd
|
|
1010
|
-
|
|
1011
|
-
elif cmd == "/model":
|
|
1012
|
-
if not arg:
|
|
1013
|
-
c.print(f"Model: {s.model}. Switch with /model <name>; list with /models.")
|
|
1014
|
-
else:
|
|
1015
|
-
try:
|
|
1016
|
-
name, spec = resolve_model(agent.ollama, arg)
|
|
1017
|
-
except (NotInstalled, OllamaError) as e:
|
|
1018
|
-
c.print(f"[red]{e}[/]")
|
|
1019
|
-
return True
|
|
1020
|
-
s.model = name
|
|
1021
|
-
s.num_batch = spec.num_batch if spec and name == spec.local_name else None
|
|
1022
|
-
if spec: # size the context for the new model: largest window that fits this machine
|
|
1023
|
-
s.context = limits.cap(name, spec.fit(hardware)[0] or MIN_USEFUL_CONTEXT)
|
|
1024
|
-
elif backends.is_openai(agent.ollama):
|
|
1025
|
-
s.context = backends.context_for(agent.ollama, name, None)[0] # the server's window
|
|
1026
|
-
agent._vision = False # the model that looks at images goes with the model: decide again
|
|
1027
|
-
agent.messages[0]["content"] = agent.system_prompt()
|
|
1028
|
-
c.print(f"[green]Switched to {name}[/] (context {format_tokens(s.context)}).")
|
|
1029
|
-
warning = residency.dense_warning(agent.ollama, name, s.context, hardware)
|
|
1030
|
-
if warning:
|
|
1031
|
-
agent.say(warning, warning=True)
|
|
1079
|
+
elif cmd in ("/models", "/model"):
|
|
1080
|
+
model_command(agent, arg, hardware)
|
|
1032
1081
|
elif cmd == "/think":
|
|
1033
1082
|
s.think = (arg == "on") if arg in ("on", "off") else not s.think
|
|
1034
1083
|
c.print(f"Reasoning {'on' if s.think else 'off'}.")
|
|
@@ -347,3 +347,17 @@ def test_a_reply_cut_off_by_the_server_says_so(server):
|
|
|
347
347
|
assert chunks[-1]["done_reason"] == "length"
|
|
348
348
|
FakeServer.replies = [sse(delta(content="Done."), USAGE)]
|
|
349
349
|
assert list(OpenAICompatible(server).chat_stream({"model": "m", "messages": []}))[-1]["done_reason"] == "stop"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_served_models_are_numbered_to_pick_from(server):
|
|
353
|
+
import io
|
|
354
|
+
|
|
355
|
+
from rich.console import Console
|
|
356
|
+
|
|
357
|
+
from lcode import cli
|
|
358
|
+
|
|
359
|
+
client = OpenAICompatible(server)
|
|
360
|
+
out = Console(file=io.StringIO(), width=120)
|
|
361
|
+
names = cli.print_models(client, None, MODELS[0], numbered=True, out=out)
|
|
362
|
+
assert names == cli.model_choices(client) == ["google/gemma-4-12b", "qwen/qwen3.6-35b-a3b"] # no embeddings
|
|
363
|
+
assert " 2 qwen/qwen3.6-35b-a3b current" in out.file.getvalue()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""/models and /model: a numbered list to switch from, without typing the model's name."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from conftest import output
|
|
6
|
+
from lcode import catalog, cli
|
|
7
|
+
from lcode.hardware import Hardware
|
|
8
|
+
from lcode.repl import handle_command
|
|
9
|
+
|
|
10
|
+
LAPTOP = Hardware("linux", "x", 31, "RTX 4080 Laptop", 12)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def answer(monkeypatch, text):
|
|
14
|
+
asked = []
|
|
15
|
+
|
|
16
|
+
def ask(prompt):
|
|
17
|
+
asked.append(prompt)
|
|
18
|
+
return text
|
|
19
|
+
|
|
20
|
+
monkeypatch.setattr("builtins.input", ask)
|
|
21
|
+
return asked
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def number_of(name: str) -> str:
|
|
25
|
+
keys = [spec.key for spec in catalog.load()]
|
|
26
|
+
return str(keys.index(name) + 1) if name in keys else str(len(keys) + 1) # custom:7b comes after the catalog
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_models_lists_numbers_and_switches_to_the_one_picked(agent, monkeypatch):
|
|
30
|
+
asked = answer(monkeypatch, number_of("custom:7b"))
|
|
31
|
+
handle_command(agent, "/models", LAPTOP)
|
|
32
|
+
out = output(agent)
|
|
33
|
+
assert "Switch to which model? (number or name, Enter to keep lcode-qwen3.6-35b)" in asked[0]
|
|
34
|
+
[row] = [line for line in out.splitlines() if "custom:7b" in line and "│" in line]
|
|
35
|
+
assert row.split("│")[1].strip() == number_of("custom:7b") and "catalog" in out
|
|
36
|
+
assert agent.settings.model == "custom:7b" and "Switched to custom:7b" in output(agent)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_model_with_no_name_shows_the_same_list(agent, monkeypatch):
|
|
40
|
+
answer(monkeypatch, "")
|
|
41
|
+
handle_command(agent, "/model", LAPTOP)
|
|
42
|
+
assert "Models for this machine" in output(agent) and "Keeping lcode-qwen3.6-35b." in output(agent)
|
|
43
|
+
assert agent.settings.model == "lcode-qwen3.6-35b"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_a_number_or_a_name_switches_without_the_list(agent, monkeypatch):
|
|
47
|
+
monkeypatch.setattr("builtins.input", lambda _: pytest.fail("should not ask"))
|
|
48
|
+
handle_command(agent, f"/model {number_of('custom:7b')}", LAPTOP)
|
|
49
|
+
assert agent.settings.model == "custom:7b" and "Models for this machine" not in output(agent)
|
|
50
|
+
handle_command(agent, "/model qwen3.6-35b", LAPTOP)
|
|
51
|
+
assert agent.settings.model == "lcode-qwen3.6-35b" and agent.settings.context == 262144
|
|
52
|
+
handle_command(agent, "/models 1", LAPTOP)
|
|
53
|
+
assert "Already using lcode-qwen3.6-35b" in output(agent)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_a_model_that_isnt_installed_says_how_to_get_it(agent, monkeypatch):
|
|
57
|
+
answer(monkeypatch, number_of("qwen3.8-27b"))
|
|
58
|
+
handle_command(agent, "/models", LAPTOP)
|
|
59
|
+
assert "lcode setup qwen3.8-27b" in output(agent) and agent.settings.model == "lcode-qwen3.6-35b"
|
|
60
|
+
handle_command(agent, "/model 99", LAPTOP)
|
|
61
|
+
assert "There's no model 99 in the list" in " ".join(output(agent).split())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_installed_models_that_cant_call_tools_are_left_out(agent):
|
|
65
|
+
installed = {"lcode-qwen3.6-35b", "custom:7b", "embed:1", "embed:1:latest", "chat-only:3b"}
|
|
66
|
+
agent.ollama.installed_names = lambda: installed
|
|
67
|
+
agent.ollama.capabilities["embed:1"] = ["tools", "thinking", "embedding"] # what Ollama says of qwen3-embedding
|
|
68
|
+
agent.ollama.capabilities["chat-only:3b"] = ["completion"]
|
|
69
|
+
choices = cli.model_choices(agent.ollama)
|
|
70
|
+
assert choices[-1] == "custom:7b" and "embed:1" not in choices and "chat-only:3b" not in choices
|
|
71
|
+
assert choices[: len(catalog.load())] == [spec.key for spec in catalog.load()]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_the_shell_command_keeps_its_table_without_numbers(monkeypatch):
|
|
75
|
+
import io
|
|
76
|
+
|
|
77
|
+
from rich.console import Console
|
|
78
|
+
|
|
79
|
+
from conftest import FakeOllama
|
|
80
|
+
|
|
81
|
+
wide = Console(file=io.StringIO(), width=160)
|
|
82
|
+
names = cli.print_models(FakeOllama(), LAPTOP, "qwen3.6-35b", out=wide)
|
|
83
|
+
out = wide.file.getvalue()
|
|
84
|
+
assert names[0] == "qwen3.6-35b" and "Install one with" in out and " # " not in out.splitlines()[2]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_context_says_why_larger_windows_are_not_offered(agent, monkeypatch):
|
|
88
|
+
answer(monkeypatch, "")
|
|
89
|
+
handle_command(agent, "/context", LAPTOP)
|
|
90
|
+
text = " ".join(output(agent).split())
|
|
91
|
+
assert "Not offered: 512K and 1M. lcode-qwen3.6-35b was trained on up to 256K tokens" in text
|
|
92
|
+
agent.ollama.max_ctx = 1048576
|
|
93
|
+
handle_command(agent, "/context", LAPTOP)
|
|
94
|
+
assert " ".join(output(agent).split()).count("Not offered") == 1
|
|
@@ -159,6 +159,6 @@ def test_switching_models_picks_the_vision_model_again(make_agent, monkeypatch):
|
|
|
159
159
|
|
|
160
160
|
agent = make_agent()
|
|
161
161
|
agent._vision = "qwen3.8:27b" # decided for the previous model
|
|
162
|
-
monkeypatch.setattr("lcode.cli.resolve_model", lambda ollama, name: ("
|
|
163
|
-
handle_command(agent, "/model
|
|
162
|
+
monkeypatch.setattr("lcode.cli.resolve_model", lambda ollama, name: ("custom:7b", None))
|
|
163
|
+
handle_command(agent, "/model custom:7b", LAPTOP)
|
|
164
164
|
assert agent._vision != "qwen3.8:27b" # decided again for the new model (here: none can see)
|
|
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
|
|
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
|
|
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
|
|
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
|