lcode-cli 0.36.0__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.
Files changed (177) hide show
  1. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/CHANGELOG.md +15 -1
  2. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/PKG-INFO +1 -1
  3. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/configuration.md +1 -1
  4. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/models.md +98 -0
  5. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/quickstart.md +1 -1
  6. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/usage.md +5 -2
  7. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/__init__.py +1 -1
  8. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/agent.py +3 -2
  9. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/cli.py +8 -0
  10. lcode_cli-0.37.0/src/lcode/cloud.py +46 -0
  11. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/escalate.py +14 -2
  12. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/ollama.py +6 -0
  13. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/repl.py +8 -0
  14. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/residency.py +3 -0
  15. lcode_cli-0.37.0/tests/test_cloud.py +86 -0
  16. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.editorconfig +0 -0
  17. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.github/CODEOWNERS +0 -0
  18. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  19. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  20. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  21. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  22. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  23. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.github/dependabot.yml +0 -0
  24. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.github/workflows/ci.yml +0 -0
  25. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.github/workflows/docs.yml +0 -0
  26. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.github/workflows/release.yml +0 -0
  27. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.gitignore +0 -0
  28. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/.pre-commit-config.yaml +0 -0
  29. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/AGENTS.md +0 -0
  30. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/CODE_OF_CONDUCT.md +0 -0
  31. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/CONTRIBUTING.md +0 -0
  32. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/LICENSE +0 -0
  33. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/README.md +0 -0
  34. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/SECURITY.md +0 -0
  35. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/action.yml +0 -0
  36. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/agents.md +0 -0
  37. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/assets/demo.svg +0 -0
  38. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/assets/extra.css +0 -0
  39. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/automation.md +0 -0
  40. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/bench.md +0 -0
  41. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/changelog.md +0 -0
  42. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/commands.md +0 -0
  43. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/contributing.md +0 -0
  44. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/editors.md +0 -0
  45. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/git.md +0 -0
  46. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/hooks.md +0 -0
  47. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/how-it-works.md +0 -0
  48. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/index.md +0 -0
  49. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/installation.md +0 -0
  50. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/lsp.md +0 -0
  51. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/mcp.md +0 -0
  52. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/memory.md +0 -0
  53. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/sandbox.md +0 -0
  54. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/search.md +0 -0
  55. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/servers.md +0 -0
  56. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/docs/troubleshooting.md +0 -0
  57. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/examples/README.md +0 -0
  58. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/examples/api/fix_tests.py +0 -0
  59. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/examples/commands/review.md +0 -0
  60. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/examples/skills/commit-message/SKILL.md +0 -0
  61. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
  62. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/install.sh +0 -0
  63. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/mkdocs.yml +0 -0
  64. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/overrides/main.html +0 -0
  65. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/pyproject.toml +0 -0
  66. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/scripts/make_logos.py +0 -0
  67. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/scripts/record_demo.py +0 -0
  68. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/scripts/social-preview.html +0 -0
  69. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/__main__.py +0 -0
  70. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/acp.py +0 -0
  71. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/action.py +0 -0
  72. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/api.py +0 -0
  73. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/backends.py +0 -0
  74. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/bench.py +0 -0
  75. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/bench_backend.py +0 -0
  76. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/bench_runtime.py +0 -0
  77. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/catalog.py +0 -0
  78. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/checkpoints.py +0 -0
  79. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/checks.py +0 -0
  80. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/clipboard.py +0 -0
  81. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/codesearch.py +0 -0
  82. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/config.py +0 -0
  83. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/context.py +0 -0
  84. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/extensions.py +0 -0
  85. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/frontmatter.py +0 -0
  86. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/gitflow.py +0 -0
  87. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/hardware.py +0 -0
  88. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/hooks.py +0 -0
  89. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/jobs.py +0 -0
  90. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/limits.py +0 -0
  91. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/lsp.py +0 -0
  92. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/matching.py +0 -0
  93. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/mcp/__init__.py +0 -0
  94. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/mcp/auth.py +0 -0
  95. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/mcp/catalog.py +0 -0
  96. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/mcp/catalog.toml +0 -0
  97. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/mcp/client.py +0 -0
  98. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/mcp/commands.py +0 -0
  99. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/mcp/config.py +0 -0
  100. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/mcp/http.py +0 -0
  101. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/mcp/manager.py +0 -0
  102. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/mcp/protocol.py +0 -0
  103. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/mcp/server.py +0 -0
  104. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/mcp/stdio.py +0 -0
  105. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/memory.py +0 -0
  106. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/models.toml +0 -0
  107. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/notify.py +0 -0
  108. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/permissions.py +0 -0
  109. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/planning.py +0 -0
  110. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/render.py +0 -0
  111. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/repeats.py +0 -0
  112. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/repomap.py +0 -0
  113. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/review.py +0 -0
  114. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/sandbox.py +0 -0
  115. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/screenshots.py +0 -0
  116. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/secretscan.py +0 -0
  117. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/servers/__init__.py +0 -0
  118. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/servers/encord.py +0 -0
  119. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/servers/valohai.py +0 -0
  120. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/servers/xai.py +0 -0
  121. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/sessions.py +0 -0
  122. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/steer.py +0 -0
  123. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/subagents.py +0 -0
  124. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/tools.py +0 -0
  125. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/unreachable.py +0 -0
  126. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/update.py +0 -0
  127. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/vision.py +0 -0
  128. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/src/lcode/web.py +0 -0
  129. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/conftest.py +0 -0
  130. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/fake_lsp_server.py +0 -0
  131. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/fake_mcp_server.py +0 -0
  132. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_acp.py +0 -0
  133. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_action.py +0 -0
  134. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_agent.py +0 -0
  135. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_api.py +0 -0
  136. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_backends.py +0 -0
  137. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_bench.py +0 -0
  138. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_catalog.py +0 -0
  139. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_checkpoints.py +0 -0
  140. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_checks.py +0 -0
  141. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_cli.py +0 -0
  142. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_clipboard.py +0 -0
  143. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_codesearch.py +0 -0
  144. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_config.py +0 -0
  145. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_context.py +0 -0
  146. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_context_picker.py +0 -0
  147. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_escalate.py +0 -0
  148. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_extensions.py +0 -0
  149. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_gitflow.py +0 -0
  150. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_hooks.py +0 -0
  151. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_jobs.py +0 -0
  152. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_lsp.py +0 -0
  153. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_matching.py +0 -0
  154. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_mcp.py +0 -0
  155. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_memory.py +0 -0
  156. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_model_picker.py +0 -0
  157. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_permissions.py +0 -0
  158. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_plan_mode.py +0 -0
  159. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_repeats.py +0 -0
  160. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_repomap.py +0 -0
  161. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_residency.py +0 -0
  162. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_review.py +0 -0
  163. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_robustness.py +0 -0
  164. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_sandbox.py +0 -0
  165. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_screenshots.py +0 -0
  166. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_secretscan.py +0 -0
  167. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_servers.py +0 -0
  168. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_sessions.py +0 -0
  169. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_steer.py +0 -0
  170. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_stuck.py +0 -0
  171. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_subagents.py +0 -0
  172. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_tools.py +0 -0
  173. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_update.py +0 -0
  174. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_vision.py +0 -0
  175. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_web.py +0 -0
  176. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/tests/test_xai.py +0 -0
  177. {lcode_cli-0.36.0 → lcode_cli-0.37.0}/uv.lock +0 -0
@@ -6,6 +6,19 @@ 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
+
9
22
  ## [0.36.0] - 2026-10-18
10
23
 
11
24
  ### Changed
@@ -810,7 +823,8 @@ First public release.
810
823
  - `AGENTS.md` project instructions and `/init` to generate them.
811
824
  - One-line installer for Linux and macOS.
812
825
 
813
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.36.0...HEAD
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
814
828
  [0.36.0]: https://github.com/nasser1941/lcode/compare/v0.35.1...v0.36.0
815
829
  [0.35.1]: https://github.com/nasser1941/lcode/compare/v0.35.0...v0.35.1
816
830
  [0.35.0]: https://github.com/nasser1941/lcode/compare/v0.34.0...v0.35.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.36.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>`, or `custom/<model>` with `escalate_url` |
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) |
@@ -85,6 +85,104 @@ context; set `--context` yourself. If a model prints tool calls as text instead
85
85
  lcode tries to recover them, but results are usually worse. To propose a model for the catalog, see
86
86
  [Contributing](contributing.md#adding-a-model-to-the-catalog).
87
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
+
88
186
  ## Choosing a context window
89
187
 
90
188
  The **context window** is how much text the model can see at once: the conversation, files it has
@@ -12,7 +12,7 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.36.0 — local coding agent │
15
+ │ lcode v0.37.0 — local coding agent │
16
16
  │ │
17
17
  │ model lcode-qwen3.6-35b │
18
18
  │ context 256K tokens │
@@ -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, or a bigger local model. The local model gets the advice
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
@@ -3,7 +3,7 @@
3
3
  From a program: `from lcode import Session` (see lcode.api).
4
4
  """
5
5
 
6
- __version__ = "0.36.0"
6
+ __version__ = "0.37.0"
7
7
 
8
8
  API = ("Session", "Result", "Options", "SetupError")
9
9
 
@@ -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
@@ -384,6 +385,13 @@ def cmd_doctor(args) -> None:
384
385
  try:
385
386
  model, spec = resolve_model(ollama, cfg["model"])
386
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
+ )
387
395
  ctx, note = choose_context(ollama, model, spec, cfg["context"], hw)
388
396
  if spec:
389
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 self.provider in ("openai", "anthropic", "xai") or (self.provider == "custom" and not self.local)
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 re.sub(r"^https?://", "", redact(self.url)).split("/")[0]
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():
@@ -45,6 +45,8 @@ from lcode import memory as memory_notes
45
45
  from lcode.agent import AUTO_COMPACT_RATIO, INIT_PROMPT, Agent
46
46
  from lcode.catalog import MIN_USEFUL_CONTEXT
47
47
  from lcode.checkpoints import Checkpoint, CheckpointError, Restore
48
+ from lcode.cloud import is_cloud
49
+ from lcode.cloud import notice as cloud_notice
48
50
  from lcode.config import PERMISSION_MODES, STATE_DIR, ConfigError, format_tokens, parse_context
49
51
  from lcode.hardware import Hardware
50
52
  from lcode.ollama import OllamaError
@@ -876,6 +878,8 @@ def switch_model(agent: Agent, wanted: str, hardware: Hardware) -> None:
876
878
  agent._vision = False # the model that looks at images goes with the model: decide again
877
879
  agent.messages[0]["content"] = agent.system_prompt()
878
880
  c.print(f"[green]Switched to {name}[/] (context {format_tokens(s.context)}).")
881
+ if is_cloud(name):
882
+ agent.say(cloud_notice(name))
879
883
  warning = residency.dense_warning(agent.ollama, name, s.context, hardware)
880
884
  if warning:
881
885
  agent.say(warning, warning=True)
@@ -1256,6 +1260,8 @@ def repl(agent: Agent, prompt: str | None, hardware: Hardware, cont: bool = Fals
1256
1260
  run_safely(agent, prompt)
1257
1261
  return
1258
1262
  banner(agent)
1263
+ if is_cloud(agent.settings.model):
1264
+ agent.say(cloud_notice(agent.settings.model))
1259
1265
  warning = residency.dense_warning(agent.ollama, agent.settings.model, agent.settings.context, hardware)
1260
1266
  if warning:
1261
1267
  agent.say(warning, warning=True)
@@ -1286,6 +1292,8 @@ def repl(agent: Agent, prompt: str | None, hardware: Hardware, cont: bool = Fals
1286
1292
 
1287
1293
  def preload(agent: Agent, session: PromptSession | None = None) -> None:
1288
1294
  """Start loading the model in the background so the first answer comes sooner; the status bar says so."""
1295
+ if is_cloud(agent.settings.model):
1296
+ return # it runs on ollama.com: there's nothing to load here
1289
1297
  where = residency.placement(agent.ollama, agent.settings.model)
1290
1298
  if where is not None and (not where.context or where.context == agent.settings.context):
1291
1299
  return # already in memory
@@ -15,6 +15,7 @@ import time
15
15
  from dataclasses import dataclass
16
16
 
17
17
  from lcode import catalog
18
+ from lcode.cloud import is_cloud
18
19
  from lcode.ollama import OllamaError
19
20
 
20
21
  GB = 1e9
@@ -97,6 +98,8 @@ def expected_share(model: str, context: int, hardware) -> float | None:
97
98
 
98
99
  def dense_warning(ollama, model: str, context: int, hardware) -> str:
99
100
  """For a dense model that won't fit on the GPU: what that means and what to do. Empty otherwise."""
101
+ if is_cloud(model):
102
+ return "" # it runs on ollama.com
100
103
  if hardware is None or hardware.unified or is_moe(ollama, model) is not False:
101
104
  return ""
102
105
  spec = catalog.find(model)
@@ -0,0 +1,86 @@
1
+ """Ollama's cloud models (tags like minimax-m3.1-flash:cloud): lcode says where they run and what they cost,
2
+ skips what only makes sense for a local model, and says how to sign in when Ollama refuses them."""
3
+
4
+ import threading
5
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
6
+
7
+ import pytest
8
+
9
+ from conftest import output
10
+ from lcode import cloud, escalate, residency
11
+ from lcode.hardware import Hardware
12
+ from lcode.ollama import Ollama, OllamaError
13
+ from lcode.repl import handle_command, preload
14
+
15
+ LAPTOP = Hardware("linux", "x", 31, "RTX 4080 Laptop", 12)
16
+
17
+
18
+ def test_cloud_tags_are_recognized():
19
+ for name in ("minimax-m3.1-flash:cloud", "deepseek-v4-pro:0813-cloud", "mistral-large-3:675b-cloud", "X:CLOUD"):
20
+ assert cloud.is_cloud(name), name
21
+ for name in ("qwen3.6:35b-a3b-coding", "lcode-qwen3.6-35b", "my-cloud-model:latest", "cloud", None, ""):
22
+ assert not cloud.is_cloud(name), name
23
+
24
+
25
+ class Refuses(BaseHTTPRequestHandler):
26
+ def log_message(self, *args):
27
+ pass
28
+
29
+ def do_POST(self):
30
+ body = b'{"error":"Unauthorized"}'
31
+ self.send_response(401)
32
+ self.send_header("Content-Type", "application/json")
33
+ self.send_header("Content-Length", str(len(body)))
34
+ self.end_headers()
35
+ self.wfile.write(body)
36
+
37
+
38
+ @pytest.fixture
39
+ def refusing_ollama(monkeypatch):
40
+ monkeypatch.setattr(cloud, "signin_command", lambda host: "docker exec -it my-ollama ollama signin")
41
+ server = ThreadingHTTPServer(("127.0.0.1", 0), Refuses)
42
+ threading.Thread(target=server.serve_forever, daemon=True).start()
43
+ yield Ollama(f"http://127.0.0.1:{server.server_address[1]}")
44
+ server.shutdown()
45
+
46
+
47
+ def test_an_ollama_that_isnt_signed_in_says_how_to_fix_it(refusing_ollama):
48
+ payload = {"model": "minimax-m3.1-flash:cloud", "messages": [{"role": "user", "content": "hi"}]}
49
+ with pytest.raises(OllamaError) as streamed:
50
+ list(refusing_ollama.chat_stream(payload))
51
+ with pytest.raises(OllamaError) as asked:
52
+ refusing_ollama.chat(payload)
53
+ for error in (streamed.value, asked.value):
54
+ assert "runs in Ollama's cloud (ollama.com)" in str(error)
55
+ assert "`docker exec -it my-ollama ollama signin`" in str(error)
56
+ with pytest.raises(OllamaError, match="Ollama error 401"): # a local model keeps the plain error
57
+ list(refusing_ollama.chat_stream({**payload, "model": "qwen3.6:35b-a3b-coding"}))
58
+
59
+
60
+ def test_the_stronger_model_panel_tells_where_a_cloud_model_runs_and_what_it_costs():
61
+ t = escalate.target("ollama/minimax-m3.1-flash:cloud")
62
+ assert t.label == "minimax-m3.1-flash:cloud (Ollama cloud)" and t.cloud
63
+ assert t.where == "ollama.com, through this machine's Ollama"
64
+ assert escalate.cost_line(t, 12000, None).startswith("unknown: set escalate_price")
65
+ assert escalate.cost_line(t, 12000, (0.15, 0.6)).startswith("about $0.002 to $0.004")
66
+ remote = escalate.target("ollama/glm-5.3-flash:cloud", "http://gpu-box:11434")
67
+ assert remote.where == "ollama.com, through gpu-box:11434"
68
+ local = escalate.target("ollama/qwen3.8-27b")
69
+ assert not local.cloud and local.where == "this machine"
70
+ assert escalate.cost_line(local, 12000, None) == "free: it runs on this machine"
71
+
72
+
73
+ def test_a_cloud_model_skips_what_only_a_local_model_needs(make_agent):
74
+ agent = make_agent(model="minimax-m3.1-flash:cloud")
75
+ agent.ollama.load = lambda *a, **k: pytest.fail("nothing to load: it runs on ollama.com")
76
+ preload(agent)
77
+ assert agent.before_request(progress=None) is None
78
+ assert residency.dense_warning(agent.ollama, "minimax-m3.1-flash:cloud", 262144, LAPTOP) == ""
79
+
80
+
81
+ def test_switching_to_a_cloud_model_says_where_the_code_goes(agent, monkeypatch):
82
+ agent.ollama.installed_names = lambda: {"lcode-qwen3.6-35b", "minimax-m3.1-flash:cloud"}
83
+ handle_command(agent, "/model minimax-m3.1-flash:cloud", LAPTOP)
84
+ text = " ".join(output(agent).split())
85
+ assert "Switched to minimax-m3.1-flash:cloud" in text
86
+ assert "runs in Ollama's cloud: what lcode sends (your requests and the files it reads) goes to ollama.com" in text
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