lcode-cli 0.35.1__tar.gz → 0.37.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.37.0}/CHANGELOG.md +27 -1
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/PKG-INFO +1 -1
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/configuration.md +1 -1
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/models.md +112 -1
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/quickstart.md +1 -1
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/usage.md +6 -3
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/__init__.py +1 -1
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/agent.py +3 -2
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/cli.py +89 -21
- lcode_cli-0.37.0/src/lcode/cloud.py +46 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/escalate.py +14 -2
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/ollama.py +6 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/repl.py +88 -31
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/residency.py +3 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_backends.py +14 -0
- lcode_cli-0.37.0/tests/test_cloud.py +86 -0
- lcode_cli-0.37.0/tests/test_model_picker.py +94 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_residency.py +2 -2
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.editorconfig +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.github/CODEOWNERS +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.github/dependabot.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.github/workflows/ci.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.github/workflows/docs.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.github/workflows/release.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.gitignore +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/.pre-commit-config.yaml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/AGENTS.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/CODE_OF_CONDUCT.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/CONTRIBUTING.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/LICENSE +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/README.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/SECURITY.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/action.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/agents.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/assets/demo.svg +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/assets/extra.css +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/automation.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/bench.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/changelog.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/commands.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/contributing.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/editors.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/git.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/hooks.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/how-it-works.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/index.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/installation.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/lsp.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/mcp.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/memory.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/sandbox.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/search.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/servers.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/docs/troubleshooting.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/examples/README.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/examples/api/fix_tests.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/examples/commands/review.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/examples/skills/commit-message/SKILL.md +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/install.sh +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/mkdocs.yml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/overrides/main.html +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/pyproject.toml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/scripts/make_logos.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/scripts/record_demo.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/scripts/social-preview.html +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/__main__.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/acp.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/action.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/api.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/backends.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/bench.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/bench_backend.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/bench_runtime.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/catalog.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/checkpoints.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/checks.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/clipboard.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/codesearch.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/config.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/context.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/extensions.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/frontmatter.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/gitflow.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/hardware.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/hooks.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/jobs.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/limits.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/lsp.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/matching.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/mcp/__init__.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/mcp/auth.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/mcp/catalog.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/mcp/catalog.toml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/mcp/client.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/mcp/commands.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/mcp/config.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/mcp/http.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/mcp/manager.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/mcp/protocol.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/mcp/server.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/mcp/stdio.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/memory.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/models.toml +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/notify.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/permissions.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/planning.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/render.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/repeats.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/repomap.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/review.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/sandbox.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/screenshots.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/secretscan.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/servers/__init__.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/servers/encord.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/servers/valohai.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/servers/xai.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/sessions.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/steer.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/subagents.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/tools.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/unreachable.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/update.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/vision.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/src/lcode/web.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/conftest.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/fake_lsp_server.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/fake_mcp_server.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_acp.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_action.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_agent.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_api.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_bench.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_catalog.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_checkpoints.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_checks.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_cli.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_clipboard.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_codesearch.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_config.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_context.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_context_picker.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_escalate.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_extensions.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_gitflow.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_hooks.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_jobs.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_lsp.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_matching.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_mcp.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_memory.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_permissions.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_plan_mode.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_repeats.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_repomap.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_review.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_robustness.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_sandbox.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_screenshots.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_secretscan.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_servers.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_sessions.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_steer.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_stuck.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_subagents.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_tools.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_update.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_vision.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_web.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/tests/test_xai.py +0 -0
- {lcode_cli-0.35.1 → lcode_cli-0.37.0}/uv.lock +0 -0
|
@@ -6,6 +6,30 @@ All notable changes to lcode are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.37.0] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Ollama's cloud models (tags ending in `cloud`, such as `minimax-m3.1-flash:cloud`) work as the
|
|
14
|
+
main model or as the stronger model when stuck, and lcode says what they are: what it sends goes
|
|
15
|
+
to ollama.com and is billed per token. The stronger-model panel said "free, on this machine" for
|
|
16
|
+
them; it now says ollama.com and shows the cost. A request Ollama refuses because it isn't signed
|
|
17
|
+
in says to run `ollama signin` (in Docker, with your container's name), and lcode skips the
|
|
18
|
+
local loading and GPU checks for them. The docs explain the setup and list the cloud models that
|
|
19
|
+
call tools, with prices and published coding scores: see
|
|
20
|
+
[Cloud models through Ollama](https://nasser1941.github.io/lcode/models/#cloud-models-through-ollama).
|
|
21
|
+
|
|
22
|
+
## [0.36.0] - 2026-10-18
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `/models` and `/model` show a numbered list of models, and you switch by typing a number: no
|
|
27
|
+
second `/model <name>`. `/model 2` switches straight away, and a name still works. The list ends
|
|
28
|
+
with the other Ollama models you've installed that can call tools, and a model that isn't
|
|
29
|
+
installed says how to get it.
|
|
30
|
+
- `/context` says why it stops at the model's largest window (256K for qwen3.6-35b): Ollama caps the
|
|
31
|
+
window at what the model was trained on, whatever the memory, so 512K or 1M would only look larger.
|
|
32
|
+
|
|
9
33
|
## [0.35.1] - 2026-10-15
|
|
10
34
|
|
|
11
35
|
### Fixed
|
|
@@ -799,7 +823,9 @@ First public release.
|
|
|
799
823
|
- `AGENTS.md` project instructions and `/init` to generate them.
|
|
800
824
|
- One-line installer for Linux and macOS.
|
|
801
825
|
|
|
802
|
-
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.
|
|
826
|
+
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.37.0...HEAD
|
|
827
|
+
[0.37.0]: https://github.com/nasser1941/lcode/compare/v0.36.0...v0.37.0
|
|
828
|
+
[0.36.0]: https://github.com/nasser1941/lcode/compare/v0.35.1...v0.36.0
|
|
803
829
|
[0.35.1]: https://github.com/nasser1941/lcode/compare/v0.35.0...v0.35.1
|
|
804
830
|
[0.35.0]: https://github.com/nasser1941/lcode/compare/v0.34.0...v0.35.0
|
|
805
831
|
[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.37.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/
|
|
@@ -45,7 +45,7 @@ lcode config path # print the file location
|
|
|
45
45
|
| `notify_after` | `30` | Seconds a request runs before it notifies |
|
|
46
46
|
| `review` | `off` | Before the answer, [review a change](usage.md#a-review-before-the-answer) against the request and send problems back to the model: `auto` or `off` |
|
|
47
47
|
| `checks` | `auto` | Run the project's [checks](usage.md#checks-before-done) after a request changed files: `auto` (found from the project), `off`, or a command |
|
|
48
|
-
| `escalate_model` | — | A [stronger model](usage.md#a-stronger-model-when-stuck) to ask, with your OK, when the model is stuck: `anthropic/…`, `openai/…`, `xai/…` (keys from `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`), `ollama/<model
|
|
48
|
+
| `escalate_model` | — | A [stronger model](usage.md#a-stronger-model-when-stuck) to ask, with your OK, when the model is stuck: `anthropic/…`, `openai/…`, `xai/…` (keys from `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`), `ollama/<model>` (a local model, or an [Ollama cloud model](models.md#cloud-models-through-ollama) like `ollama/glm-5.3-flash:cloud`), or `custom/<model>` with `escalate_url` |
|
|
49
49
|
| `escalate_url` | — | Its server, when it isn't the provider's usual one: another Ollama, or any OpenAI-compatible API |
|
|
50
50
|
| `escalate_price` | — | Its price in dollars per million tokens, input/output (e.g. `3/15`), for the cost shown before each use |
|
|
51
51
|
| `repeat_limit` | `3` | Identical tool calls with identical results before lcode [steps in](how-it-works.md#when-the-model-repeats-itself); `0` turns it off, along with the checks for [unreachable sites](how-it-works.md#when-a-site-cant-be-reached) and [thinking in circles](how-it-works.md#when-a-reply-goes-nowhere) |
|
|
@@ -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
|
|
@@ -76,6 +85,104 @@ context; set `--context` yourself. If a model prints tool calls as text instead
|
|
|
76
85
|
lcode tries to recover them, but results are usually worse. To propose a model for the catalog, see
|
|
77
86
|
[Contributing](contributing.md#adding-a-model-to-the-catalog).
|
|
78
87
|
|
|
88
|
+
## Cloud models through Ollama
|
|
89
|
+
|
|
90
|
+
Ollama also offers models that run on its own servers, with tags that end in `cloud`, such as
|
|
91
|
+
`minimax-m3.1-flash:cloud`. Your Ollama passes the requests on, so lcode uses them like an installed
|
|
92
|
+
model: as the main model, or as the [stronger model to ask when stuck](usage.md#a-stronger-model-when-stuck).
|
|
93
|
+
What changes:
|
|
94
|
+
|
|
95
|
+
- **Your code leaves the machine.** What lcode sends (your requests and the files it reads) goes to
|
|
96
|
+
ollama.com. lcode says so when a session starts, when you switch to one, and in `lcode doctor`.
|
|
97
|
+
- **It's billed per token** to your ollama.com account (some models are free for a while).
|
|
98
|
+
- **It needs an ollama.com account** and a connection to ollama.com.
|
|
99
|
+
|
|
100
|
+
### Setting one up
|
|
101
|
+
|
|
102
|
+
1. **Sign your Ollama in** to your ollama.com account, once. It prints a link to open in the
|
|
103
|
+
browser:
|
|
104
|
+
|
|
105
|
+
=== "Ollama installed normally"
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
ollama signin
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
=== "Ollama in Docker"
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
docker exec -it <container> ollama signin
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`lcode doctor` shows this command with your container's name once a cloud model is set.
|
|
118
|
+
|
|
119
|
+
2. **Add the model.** This stores a small reference, not the weights:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
ollama pull minimax-m3.1-flash:cloud # in Docker: docker exec <container> ollama pull …
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
3. **Use it** as the main model, or as the stronger model when stuck:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
lcode --model minimax-m3.1-flash:cloud --context 256k # or pick it by number in /models
|
|
129
|
+
|
|
130
|
+
lcode config set escalate_model ollama/minimax-m3.1-flash:cloud
|
|
131
|
+
lcode config set escalate_price 0.15/0.60 # its price in/out, for the cost shown
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
As the main model, give the context yourself: lcode defaults to 32K for models outside its
|
|
135
|
+
catalog. A larger window costs more, since every step sends the conversation again (the
|
|
136
|
+
repeated part at the lower *cached* price). As the stronger model, only a short brief goes out,
|
|
137
|
+
and only when you say yes: the panel shows `Where: … at ollama.com` and the cost.
|
|
138
|
+
|
|
139
|
+
If a request fails with *this Ollama isn't signed in to an ollama.com account*, run the sign-in
|
|
140
|
+
command the message shows.
|
|
141
|
+
|
|
142
|
+
### Cheap coding models in Ollama's cloud
|
|
143
|
+
|
|
144
|
+
The cloud models that call tools, cheapest first. Prices are in dollars per million tokens, from
|
|
145
|
+
[ollama.com](https://ollama.com/search?c=cloud) on 2026-10-20. Check a model's page before you
|
|
146
|
+
rely on a price: they change.
|
|
147
|
+
|
|
148
|
+
- **TB 2.1** is the Terminal-Bench 2.1 score as published on the model's page. The makers measured
|
|
149
|
+
it themselves, in different setups, so it's a rough guide, not a ranking. A dash means the page
|
|
150
|
+
gives none.
|
|
151
|
+
- **One question** is the most a question to the stronger model costs: a 15K-token brief, plus
|
|
152
|
+
lcode's 4K-token cap on the answer.
|
|
153
|
+
- **DeepSeek** charges less off-peak. Its prices read off-peak / peak.
|
|
154
|
+
|
|
155
|
+
| Model | Input | Cached | Output | Context | TB 2.1 | One question |
|
|
156
|
+
|---|---|---|---|---|---|---|
|
|
157
|
+
| `glm-5.3-flash:cloud` | $0.15 | $0.03 | $0.50 | 1M | 84.3 | $0.004 |
|
|
158
|
+
| `minimax-m3.1-flash:cloud` | $0.15 | $0.015 | $0.60 | 976K | — | $0.005 |
|
|
159
|
+
| `deepseek-v4.1-flash:cloud` | $0.15 / $0.30 | $0.003 / $0.006 | $0.60 / $1.20 | 1M | 90.6 | $0.005–$0.009 |
|
|
160
|
+
| `minimax-m2.7:cloud` | $0.30 | $0.06 | $1.20 | 200K | — | $0.009 |
|
|
161
|
+
| `nemotron-3-ultra:cloud` | $0.10 | $0.10 | $3.00 | 256K | — | $0.014 |
|
|
162
|
+
| `mistral-large-3:675b-cloud` | $0.50 | — | $1.50 | 256K | — | $0.014 |
|
|
163
|
+
| `minimax-m3:cloud` | $0.60 | $0.12 | $2.40 | 512K | — | $0.019 |
|
|
164
|
+
| `mistral-large-4:cloud` | $0.68 | $0.07 | $2.09 | 1M | — | $0.019 |
|
|
165
|
+
| `deepseek-v4-pro:cloud` | $0.66 / $1.32 | $0.022 / $0.044 | $1.98 / $3.96 | 1M | 87.9 | $0.018–$0.036 |
|
|
166
|
+
| `kimi-k2.6:cloud` | $0.95 | $0.16 | $4.00 | 256K | — | $0.031 |
|
|
167
|
+
| `kimi-k2.7-code:cloud` | $0.95 | $0.19 | $4.00 | 256K | — | $0.031 |
|
|
168
|
+
| `muse-spark-open:cloud` | $1.25 | $0.15 | $4.25 | 1M | — | $0.036 |
|
|
169
|
+
| `glm-5.2:cloud` | $1.40 | $0.26 | $4.40 | 976K | 81.0 | $0.039 |
|
|
170
|
+
| `glm-5.3:cloud` | $1.40 | $0.26 | $4.40 | 1M | 88.2 | $0.039 |
|
|
171
|
+
| `kimi-k3:cloud` | $3.00 | $0.30 | $15.00 | 1M | 88.3 | $0.106 |
|
|
172
|
+
|
|
173
|
+
- **For the stronger model, the first three cost under a cent a question.**
|
|
174
|
+
- `deepseek-v4.1-flash` has the highest published score (90.6, DeepSeek's own measurement).
|
|
175
|
+
- `glm-5.3-flash` is the cheapest.
|
|
176
|
+
- `minimax-m3.1-flash` is free until 22 October 2026.
|
|
177
|
+
- **GLM-5.3, DeepSeek V4 Pro and Kimi K3** publish similar scores (88.2, 87.9, 88.3) for 4 to 25
|
|
178
|
+
times the price.
|
|
179
|
+
- **Not in the table:** `beam`, `gemma4:31b-cloud`, `nemotron-3-super`, `nemotron-3-nano` and
|
|
180
|
+
`clef` have cloud versions too, but list no price on their pages. Clef is an image model, not a
|
|
181
|
+
coding one.
|
|
182
|
+
|
|
183
|
+
To set a price for the cost shown, use the input/output pair from the table, e.g.
|
|
184
|
+
`lcode config set escalate_price 0.15/0.50` for `glm-5.3-flash:cloud`.
|
|
185
|
+
|
|
79
186
|
## Choosing a context window
|
|
80
187
|
|
|
81
188
|
The **context window** is how much text the model can see at once: the conversation, files it has
|
|
@@ -107,6 +214,10 @@ Context window for lcode-qwen3.6-35b
|
|
|
107
214
|
Choose a number, a size like 96k, or press Enter to keep 256K: 2
|
|
108
215
|
```
|
|
109
216
|
|
|
217
|
+
The list stops at the largest window the model was trained on, and says so: Ollama caps the window
|
|
218
|
+
there whatever the memory, so a larger setting would only look larger. For qwen3.6-35b that's 256K;
|
|
219
|
+
512K and 1M are offered for models trained on them.
|
|
220
|
+
|
|
110
221
|
Sizes accept `k` and `m` suffixes (`128k` = 131,072 tokens, `1m` = 1,048,576). When the window is
|
|
111
222
|
85% full, lcode summarizes the conversation automatically so you can keep working; `/compact` does it
|
|
112
223
|
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 |
|
|
@@ -219,7 +219,8 @@ would have to read the whole conversation again. On a small GPU that's tens of s
|
|
|
219
219
|
## A stronger model when stuck
|
|
220
220
|
|
|
221
221
|
Some problems are beyond a local model. When it's stuck, lcode can ask a stronger one for
|
|
222
|
-
advice: a cloud model with your API key,
|
|
222
|
+
advice: a cloud model with your API key, one of [Ollama's cloud models](models.md#cloud-models-through-ollama)
|
|
223
|
+
(less than a cent a question for some), or a bigger local model. The local model gets the advice
|
|
223
224
|
and carries on with its own tools. It's off until you choose the model:
|
|
224
225
|
|
|
225
226
|
```bash
|
|
@@ -227,6 +228,7 @@ lcode config set escalate_model anthropic/<model> # with ANTHROPIC_API_KEY in
|
|
|
227
228
|
lcode config set escalate_model openai/<model> # OPENAI_API_KEY
|
|
228
229
|
lcode config set escalate_model xai/<model> # XAI_API_KEY
|
|
229
230
|
lcode config set escalate_model ollama/qwen3.8-27b # a bigger local model: free, and nothing leaves
|
|
231
|
+
lcode config set escalate_model ollama/glm-5.3-flash:cloud # Ollama's cloud, after `ollama signin`
|
|
230
232
|
lcode config set escalate_price 3/15 # dollars per million tokens in/out, for the cost
|
|
231
233
|
```
|
|
232
234
|
|
|
@@ -257,7 +259,8 @@ a stronger model can't reach it either. You can also ask at any time: `/escalate
|
|
|
257
259
|
- **Keys** come from environment variables, are read when needed, and go only to their own
|
|
258
260
|
provider. lcode never stores them.
|
|
259
261
|
- **Cost.** The token count is always shown; the price only when you set `escalate_price`, since
|
|
260
|
-
prices change. A local model is free
|
|
262
|
+
prices change. A local model is free; an Ollama cloud model isn't, and the panel says it goes to
|
|
263
|
+
ollama.com.
|
|
261
264
|
- **Another server:** `escalate_url` points at another Ollama (`ollama/<model>`) or any OpenAI
|
|
262
265
|
API (`custom/<model>`, key in `LCODE_ESCALATE_API_KEY`).
|
|
263
266
|
- `lcode -p` can't ask, so it doesn't escalate unless you add `--escalate`, which is your OK for
|
|
@@ -45,6 +45,7 @@ from lcode import (
|
|
|
45
45
|
from lcode import context as context_tools
|
|
46
46
|
from lcode import memory as memory_notes
|
|
47
47
|
from lcode.checkpoints import Checkpoints
|
|
48
|
+
from lcode.cloud import is_cloud
|
|
48
49
|
from lcode.config import format_tokens
|
|
49
50
|
from lcode.jobs import Jobs
|
|
50
51
|
from lcode.mcp import McpManager
|
|
@@ -1011,8 +1012,8 @@ class Agent:
|
|
|
1011
1012
|
def before_request(self, progress: Progress) -> residency.LoadWatch | None:
|
|
1012
1013
|
"""Set the status line for what comes before the first token: loading the model if it isn't in memory
|
|
1013
1014
|
(watched until it is), then reading the prompt. Returns the watch while the model loads."""
|
|
1014
|
-
if getattr(self.ollama, "kind", "ollama") != "ollama":
|
|
1015
|
-
return None
|
|
1015
|
+
if getattr(self.ollama, "kind", "ollama") != "ollama" or is_cloud(self.settings.model):
|
|
1016
|
+
return None # another server loads models itself; an Ollama cloud model isn't loaded here at all
|
|
1016
1017
|
model, context = self.settings.model, self.settings.context
|
|
1017
1018
|
fresh = max(0, conversation_chars(self.messages) - self.sent_chars) // 3
|
|
1018
1019
|
where = residency.placement(self.ollama, model)
|
|
@@ -21,6 +21,7 @@ from rich.table import Table
|
|
|
21
21
|
from lcode import __version__, backends, catalog, config, limits, web
|
|
22
22
|
from lcode.agent import Settings
|
|
23
23
|
from lcode.catalog import ModelSpec
|
|
24
|
+
from lcode.cloud import is_cloud, signin_command
|
|
24
25
|
from lcode.config import ConfigError, format_tokens, parse_context
|
|
25
26
|
from lcode.hardware import Hardware, detect
|
|
26
27
|
from lcode.ollama import MIN_VERSION, Ollama, OllamaError, redact, version_tuple
|
|
@@ -113,44 +114,96 @@ def choose_context(
|
|
|
113
114
|
# ----------------------------------------------------------------------------- lcode models
|
|
114
115
|
|
|
115
116
|
|
|
116
|
-
def print_served(
|
|
117
|
+
def print_served(
|
|
118
|
+
client: backends.OpenAICompatible, current: str | None, numbered: bool = False, out: Console | None = None
|
|
119
|
+
) -> list[str]:
|
|
120
|
+
out = out or console
|
|
117
121
|
try:
|
|
118
122
|
served = client.chat_models()
|
|
119
123
|
current = backends.pick_model(client, current) if current else None
|
|
120
124
|
except OllamaError as e:
|
|
121
125
|
if not isinstance(e, backends.BackendError) or "doesn't serve" not in str(e):
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
out.print(f"[red]{e}[/]")
|
|
127
|
+
return []
|
|
128
|
+
served = []
|
|
129
|
+
out.print(f"[bold]Models served by {escape(client.describe())}[/]")
|
|
130
|
+
for i, name in enumerate(served, 1):
|
|
131
|
+
number = f"[cyan]{i:>3}[/] " if numbered else " "
|
|
132
|
+
out.print(f"{number}{escape(name)}" + (" [bold]current[/]" if name == current else ""))
|
|
127
133
|
if not served:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
out.print(" none listed")
|
|
135
|
+
if not numbered:
|
|
136
|
+
out.print(
|
|
137
|
+
"[dim]Models are downloaded and loaded in the server itself. Choose one with [/]lcode --model <id>[dim] "
|
|
138
|
+
"or [/]lcode config set model <id>[dim].[/]"
|
|
139
|
+
)
|
|
140
|
+
return served
|
|
133
141
|
|
|
134
142
|
|
|
135
|
-
def
|
|
143
|
+
def other_installed(ollama: Ollama, installed: set[str]) -> list[str]:
|
|
144
|
+
"""Installed Ollama models outside the catalog that can chat and call tools (so lcode can use them), by name.
|
|
145
|
+
Embedding models are left out: Ollama may list `tools` for them too, but not `completion`."""
|
|
146
|
+
known = {name for spec in catalog.load() for name in (spec.tag, spec.local_name)}
|
|
147
|
+
usable = []
|
|
148
|
+
for name in sorted({n.removesuffix(":latest") for n in installed} - known):
|
|
149
|
+
try:
|
|
150
|
+
capabilities = ollama.show(name).get("capabilities") or []
|
|
151
|
+
if "completion" in capabilities and "tools" in capabilities:
|
|
152
|
+
usable.append(name)
|
|
153
|
+
except OllamaError:
|
|
154
|
+
continue
|
|
155
|
+
return usable
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def model_choices(ollama: Ollama | None) -> list[str]:
|
|
159
|
+
"""What `/models` numbers, in its order: the catalog's keys, then other installed models that call tools.
|
|
160
|
+
For an OpenAI-compatible server, the models it serves."""
|
|
136
161
|
if ollama is not None and backends.is_openai(ollama):
|
|
137
|
-
|
|
138
|
-
|
|
162
|
+
try:
|
|
163
|
+
return ollama.chat_models() # type: ignore[union-attr]
|
|
164
|
+
except OllamaError:
|
|
165
|
+
return []
|
|
166
|
+
try:
|
|
167
|
+
installed = ollama.installed_names() if ollama else set()
|
|
168
|
+
except OllamaError:
|
|
169
|
+
installed = set()
|
|
170
|
+
return [spec.key for spec in catalog.load()] + (other_installed(ollama, installed) if ollama else [])
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def print_models(
|
|
174
|
+
ollama: Ollama | None,
|
|
175
|
+
hw: Hardware,
|
|
176
|
+
current: str | None = None,
|
|
177
|
+
numbered: bool = False,
|
|
178
|
+
out: Console | None = None,
|
|
179
|
+
) -> list[str]:
|
|
180
|
+
"""The models table; with `numbered`, a number on each row to pick it by. Returns the rows' names in order."""
|
|
181
|
+
out = out or console
|
|
182
|
+
if ollama is not None and backends.is_openai(ollama):
|
|
183
|
+
return print_served(ollama, current, numbered, out) # type: ignore[arg-type]
|
|
139
184
|
try:
|
|
140
185
|
installed = ollama.installed_names() if ollama else set()
|
|
141
186
|
except OllamaError:
|
|
142
187
|
installed = set()
|
|
143
188
|
rec = catalog.recommend(hw)
|
|
144
|
-
wide =
|
|
189
|
+
wide = out.width >= 130 # the descriptive columns don't fit an 80-column terminal
|
|
145
190
|
columns = ["Key", "Model", "Type", "Size", "Max ctx", "Fits here", "Speed", "Status"]
|
|
146
191
|
if not wide:
|
|
147
192
|
columns = [c for c in columns if c not in ("Model", "Type")]
|
|
193
|
+
if numbered:
|
|
194
|
+
columns.insert(0, "#")
|
|
148
195
|
table = Table(title=f"Models for this machine: {hw.describe()}", title_justify="left", header_style="bold")
|
|
149
196
|
for col in columns:
|
|
150
197
|
table.add_column(
|
|
151
|
-
col,
|
|
198
|
+
col,
|
|
199
|
+
no_wrap=col in ("#", "Key", "Size", "Max ctx", "Fits here"),
|
|
200
|
+
min_width=11 if col == "Status" else None,
|
|
201
|
+
justify="right" if col == "#" else "left",
|
|
202
|
+
style="cyan" if col == "#" else None,
|
|
152
203
|
)
|
|
204
|
+
choices: list[str] = []
|
|
153
205
|
for spec in catalog.load():
|
|
206
|
+
choices.append(spec.key)
|
|
154
207
|
ctx, speed = spec.fit(hw)
|
|
155
208
|
marks = []
|
|
156
209
|
if spec.local_name in installed or spec.tag in installed:
|
|
@@ -161,6 +214,7 @@ def print_models(ollama: Ollama | None, hw: Hardware, current: str | None = None
|
|
|
161
214
|
marks.append("[bold]current[/]")
|
|
162
215
|
marks.append("tested" if spec.tested else "[dim]untested[/]")
|
|
163
216
|
row = {
|
|
217
|
+
"#": str(len(choices)),
|
|
164
218
|
"Key": spec.key,
|
|
165
219
|
"Model": spec.name,
|
|
166
220
|
"Type": spec.params,
|
|
@@ -171,11 +225,18 @@ def print_models(ollama: Ollama | None, hw: Hardware, current: str | None = None
|
|
|
171
225
|
"Status": ", ".join(marks) if wide else "\n".join(marks),
|
|
172
226
|
}
|
|
173
227
|
table.add_row(*(row[c] for c in columns))
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"[
|
|
177
|
-
|
|
178
|
-
|
|
228
|
+
for name in other_installed(ollama, installed) if ollama else []:
|
|
229
|
+
choices.append(name)
|
|
230
|
+
marks = ["[green]installed[/]"] + (["[bold]current[/]"] if name == current else []) + ["[dim]not in catalog[/]"]
|
|
231
|
+
row = {"#": str(len(choices)), "Key": escape(name), "Status": ", ".join(marks) if wide else "\n".join(marks)}
|
|
232
|
+
table.add_row(*(row.get(c, "") for c in columns))
|
|
233
|
+
out.print(table)
|
|
234
|
+
if not numbered:
|
|
235
|
+
out.print(
|
|
236
|
+
"[dim]Install one with [/]lcode setup <key>[dim]. 'Fits here' is the largest context that fits in memory; "
|
|
237
|
+
"choose another with --context. Any other Ollama model with tool calling also works: lcode --model <tag>[/]"
|
|
238
|
+
)
|
|
239
|
+
return choices
|
|
179
240
|
|
|
180
241
|
|
|
181
242
|
def cmd_models(args) -> None:
|
|
@@ -324,6 +385,13 @@ def cmd_doctor(args) -> None:
|
|
|
324
385
|
try:
|
|
325
386
|
model, spec = resolve_model(ollama, cfg["model"])
|
|
326
387
|
line("Model", f"{cfg['model']} → {model}", True)
|
|
388
|
+
if is_cloud(model):
|
|
389
|
+
line(
|
|
390
|
+
"",
|
|
391
|
+
"runs in Ollama's cloud: what lcode sends goes to ollama.com, billed per token, and this Ollama "
|
|
392
|
+
f"must be signed in once: {signin_command(ollama.host)}",
|
|
393
|
+
None,
|
|
394
|
+
)
|
|
327
395
|
ctx, note = choose_context(ollama, model, spec, cfg["context"], hw)
|
|
328
396
|
if spec:
|
|
329
397
|
from lcode.subagents import parallel_slots
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Ollama's cloud models: tags like `minimax-m3.1-flash:cloud` run on ollama.com, through the local Ollama.
|
|
2
|
+
|
|
3
|
+
They look like any installed model to lcode (Ollama lists them, and they chat and call tools), but nothing about
|
|
4
|
+
them is local: what lcode sends goes to ollama.com, it's billed per token, and it needs the Ollama signed in to an
|
|
5
|
+
ollama.com account (`ollama signin`). So lcode says so where it matters (`lcode doctor`, the start of a session, the
|
|
6
|
+
stronger-model panel), skips what only makes sense for a local model (loading it into memory, GPU fit), and turns
|
|
7
|
+
Ollama's bare `Unauthorized` into the command that fixes it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_cloud(model: str | None) -> bool:
|
|
14
|
+
"""Whether an Ollama model name is one of Ollama's cloud models: its tag is `cloud` or ends with `-cloud`."""
|
|
15
|
+
name = (model or "").strip().lower()
|
|
16
|
+
if ":" not in name:
|
|
17
|
+
return False
|
|
18
|
+
tag = name.rsplit(":", 1)[1]
|
|
19
|
+
return tag == "cloud" or tag.endswith("-cloud")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def signin_command(host: str) -> str:
|
|
23
|
+
"""How to sign the Ollama at `host` in to ollama.com: inside its container when it runs in Docker."""
|
|
24
|
+
try:
|
|
25
|
+
from lcode import update
|
|
26
|
+
|
|
27
|
+
container = update.find_container(host) if update.docker_available() else None
|
|
28
|
+
except Exception: # Docker missing, or not answering: the plain command is still right for most installs
|
|
29
|
+
container = None
|
|
30
|
+
return f"docker exec -it {container.name} ollama signin" if container else "ollama signin"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def unauthorized(model: str, host: str) -> str:
|
|
34
|
+
"""The error for a cloud model that Ollama refused: what it means and the command to run."""
|
|
35
|
+
return (
|
|
36
|
+
f"{model} runs in Ollama's cloud (ollama.com), and this Ollama isn't signed in to an ollama.com account. "
|
|
37
|
+
f"Sign it in with `{signin_command(host)}`, then try again."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def notice(model: str) -> str:
|
|
42
|
+
"""What to know when a cloud model is lcode's main model."""
|
|
43
|
+
return (
|
|
44
|
+
f"{model} runs in Ollama's cloud: what lcode sends (your requests and the files it reads) goes to "
|
|
45
|
+
"ollama.com, and Ollama bills it per token."
|
|
46
|
+
)
|
|
@@ -23,6 +23,7 @@ from rich.panel import Panel
|
|
|
23
23
|
from rich.text import Text
|
|
24
24
|
|
|
25
25
|
from lcode import context as context_tools
|
|
26
|
+
from lcode.cloud import is_cloud
|
|
26
27
|
from lcode.ollama import Ollama, OllamaError, redact
|
|
27
28
|
|
|
28
29
|
if TYPE_CHECKING:
|
|
@@ -76,14 +77,23 @@ class Target:
|
|
|
76
77
|
@property
|
|
77
78
|
def cloud(self) -> bool:
|
|
78
79
|
"""Paid per token: a cloud provider, or an OpenAI-compatible API elsewhere."""
|
|
79
|
-
return
|
|
80
|
+
return (
|
|
81
|
+
self.provider in ("openai", "anthropic", "xai")
|
|
82
|
+
or (self.provider == "custom" and not self.local)
|
|
83
|
+
or (self.provider == "ollama" and is_cloud(self.model)) # an Ollama cloud model runs on ollama.com
|
|
84
|
+
)
|
|
80
85
|
|
|
81
86
|
@property
|
|
82
87
|
def where(self) -> str:
|
|
88
|
+
if self.provider == "ollama" and is_cloud(self.model):
|
|
89
|
+
return "ollama.com, through " + ("this machine's Ollama" if self.local else self._host())
|
|
83
90
|
if self.url:
|
|
84
|
-
return
|
|
91
|
+
return self._host()
|
|
85
92
|
return "this machine"
|
|
86
93
|
|
|
94
|
+
def _host(self) -> str:
|
|
95
|
+
return re.sub(r"^https?://", "", redact(self.url)).split("/")[0]
|
|
96
|
+
|
|
87
97
|
|
|
88
98
|
def target(setting: str | None, url: str | None = None) -> Target | None:
|
|
89
99
|
"""The stronger model from the settings (None: escalation is off). Raises EscalateError for a bad setting."""
|
|
@@ -105,6 +115,8 @@ def target(setting: str | None, url: str | None = None) -> Target | None:
|
|
|
105
115
|
raise EscalateError(f"escalate_model {setting!r} names no model")
|
|
106
116
|
if provider == "custom" and not url:
|
|
107
117
|
raise EscalateError("a custom/… escalate_model needs escalate_url, the server's OpenAI-compatible address")
|
|
118
|
+
if provider == "ollama" and is_cloud(model):
|
|
119
|
+
name = "Ollama cloud"
|
|
108
120
|
return Target(provider, model.strip(), (url or base).rstrip("/"), key_env, f"{model.strip()} ({name})")
|
|
109
121
|
|
|
110
122
|
|
|
@@ -9,6 +9,8 @@ from urllib.parse import urlsplit, urlunsplit
|
|
|
9
9
|
|
|
10
10
|
import requests
|
|
11
11
|
|
|
12
|
+
from lcode.cloud import is_cloud, unauthorized
|
|
13
|
+
|
|
12
14
|
MIN_VERSION = (0, 30, 0) # first release that runs the Qwen3.5/3.6 hybrid-attention architecture
|
|
13
15
|
|
|
14
16
|
|
|
@@ -49,6 +51,8 @@ class Ollama:
|
|
|
49
51
|
r = requests.post(self._url(path), json=payload, timeout=timeout)
|
|
50
52
|
except requests.RequestException as e:
|
|
51
53
|
raise OllamaError(f"cannot reach Ollama at {redact(self.host)}: {e}") from e
|
|
54
|
+
if r.status_code == 401 and is_cloud(payload.get("model")):
|
|
55
|
+
raise OllamaError(unauthorized(payload["model"], self.host))
|
|
52
56
|
if r.status_code != 200:
|
|
53
57
|
raise OllamaError(f"Ollama {path} failed ({r.status_code}): {r.text[:300]}")
|
|
54
58
|
return r.json()
|
|
@@ -164,6 +168,8 @@ class Ollama:
|
|
|
164
168
|
if on_open:
|
|
165
169
|
on_open(r)
|
|
166
170
|
with r:
|
|
171
|
+
if r.status_code == 401 and is_cloud(payload.get("model")):
|
|
172
|
+
raise OllamaError(unauthorized(payload["model"], self.host))
|
|
167
173
|
if r.status_code != 200:
|
|
168
174
|
raise OllamaError(f"Ollama error {r.status_code}: {r.text[:500]}")
|
|
169
175
|
for line in r.iter_lines():
|