virtuai-cli 1.0.3__tar.gz → 1.0.4__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.
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/PKG-INFO +3 -3
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/README.md +2 -2
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/pyproject.toml +1 -1
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/__init__.py +1 -1
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/ollama_state.py +8 -3
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/pull.py +12 -3
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/runner.py +8 -6
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli.egg-info/PKG-INFO +3 -3
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_ollama_options.py +15 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/setup.cfg +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/browser.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/__init__.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/agents.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/ask.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/command.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/history.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/sse.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/tui.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/widgets.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/config.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/executor.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/main.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/omlx_state.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/opencode.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/security.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli.egg-info/SOURCES.txt +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli.egg-info/dependency_links.txt +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli.egg-info/entry_points.txt +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli.egg-info/requires.txt +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli.egg-info/top_level.txt +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_browser.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_frame_concurrency.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_ollama_state.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_omlx.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_process_group_kill.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_pull_fallback.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_run_single_instance.py +0 -0
- {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_session_isolation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: virtuai-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Run VirtuAI deep agents on your local machine
|
|
5
5
|
Author-email: uCloudStore <lmoreno@ucloudstore.com>
|
|
6
6
|
License: Proprietary
|
|
@@ -138,7 +138,7 @@ Each model call goes to the runner of the person the turn is for; if they have n
|
|
|
138
138
|
|
|
139
139
|
At connect and then every minute: the models it has, the Ollama version, what is loaded and how much memory it really took. With `llmfit` installed it also estimates which pulled models fit this machine, so the server does not send a model to a runner that cannot hold it until it has actually run there. A model that spills to the CPU gets half the context on its next call on that machine (down to 4k).
|
|
140
140
|
|
|
141
|
-
The context sent to Ollama is
|
|
141
|
+
The context sent to Ollama is 16k by default (set on the server). A model that does not fit at that context — `qwen3:14b` on a 16 GB Mac spills 7% to the CPU — is capped at half on that machine after its first call, and the server is told in the same reply: from then on it sends prompts that fit, compacting the conversation when they would not, instead of letting Ollama cut the start of the prompt. `OLLAMA_NUM_CTX` sets a lower context on a machine with little memory. For qwen3 the runner turns Ollama's thinking mode off: left on, it reasoned for minutes before every tool call.
|
|
142
142
|
|
|
143
143
|
### oMLX
|
|
144
144
|
|
|
@@ -163,7 +163,7 @@ mean "allow more commands at once" silently launched more browsers.
|
|
|
163
163
|
| `VIRTUAI_SANDBOX_OWNER` | unset | The one person this runner serves, reported to the server so a browser action reaches *their* browser. Leave it unset on a laptop or a container shared by a team: the server then routes to it for anyone, which is the behaviour that existed before per-person sandboxes. Set it when a workspace has a sandbox per person, or actions land on whichever runner connected last. |
|
|
164
164
|
| `VIRTUAI_SERVE_MODELS` | `own` | Whose local-model turns this runner answers: `own`, `workspace` or `none`. Also set by `--serve-models`. |
|
|
165
165
|
| `OLLAMA_BASE_URL` | `http://localhost:11434` | Where to reach the local Ollama. |
|
|
166
|
-
| `OLLAMA_NUM_CTX` | unset | The context to ask Ollama for, instead of the server's (
|
|
166
|
+
| `OLLAMA_NUM_CTX` | unset | The context to ask Ollama for, instead of the server's (16k). Lower it on a machine with little memory. |
|
|
167
167
|
| `VIRTUAI_OMLX_URL` | from `~/.omlx/settings.json`, else `http://127.0.0.1:8000` | Where to reach oMLX. |
|
|
168
168
|
| `OMLX_API_KEY` | from `~/.omlx/settings.json` | oMLX's API key. Stays on this machine. |
|
|
169
169
|
|
|
@@ -109,7 +109,7 @@ Each model call goes to the runner of the person the turn is for; if they have n
|
|
|
109
109
|
|
|
110
110
|
At connect and then every minute: the models it has, the Ollama version, what is loaded and how much memory it really took. With `llmfit` installed it also estimates which pulled models fit this machine, so the server does not send a model to a runner that cannot hold it until it has actually run there. A model that spills to the CPU gets half the context on its next call on that machine (down to 4k).
|
|
111
111
|
|
|
112
|
-
The context sent to Ollama is
|
|
112
|
+
The context sent to Ollama is 16k by default (set on the server). A model that does not fit at that context — `qwen3:14b` on a 16 GB Mac spills 7% to the CPU — is capped at half on that machine after its first call, and the server is told in the same reply: from then on it sends prompts that fit, compacting the conversation when they would not, instead of letting Ollama cut the start of the prompt. `OLLAMA_NUM_CTX` sets a lower context on a machine with little memory. For qwen3 the runner turns Ollama's thinking mode off: left on, it reasoned for minutes before every tool call.
|
|
113
113
|
|
|
114
114
|
### oMLX
|
|
115
115
|
|
|
@@ -134,7 +134,7 @@ mean "allow more commands at once" silently launched more browsers.
|
|
|
134
134
|
| `VIRTUAI_SANDBOX_OWNER` | unset | The one person this runner serves, reported to the server so a browser action reaches *their* browser. Leave it unset on a laptop or a container shared by a team: the server then routes to it for anyone, which is the behaviour that existed before per-person sandboxes. Set it when a workspace has a sandbox per person, or actions land on whichever runner connected last. |
|
|
135
135
|
| `VIRTUAI_SERVE_MODELS` | `own` | Whose local-model turns this runner answers: `own`, `workspace` or `none`. Also set by `--serve-models`. |
|
|
136
136
|
| `OLLAMA_BASE_URL` | `http://localhost:11434` | Where to reach the local Ollama. |
|
|
137
|
-
| `OLLAMA_NUM_CTX` | unset | The context to ask Ollama for, instead of the server's (
|
|
137
|
+
| `OLLAMA_NUM_CTX` | unset | The context to ask Ollama for, instead of the server's (16k). Lower it on a machine with little memory. |
|
|
138
138
|
| `VIRTUAI_OMLX_URL` | from `~/.omlx/settings.json`, else `http://127.0.0.1:8000` | Where to reach oMLX. |
|
|
139
139
|
| `OMLX_API_KEY` | from `~/.omlx/settings.json` | oMLX's API key. Stays on this machine. |
|
|
140
140
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""VirtuAI local CLI."""
|
|
2
|
-
__version__ = "1.0.
|
|
2
|
+
__version__ = "1.0.4"
|
|
@@ -120,7 +120,7 @@ async def after_call(model: str) -> None:
|
|
|
120
120
|
info = (state.get("loaded") or {}).get(model)
|
|
121
121
|
if not info or info.get("on_gpu_pct", 100) >= 100:
|
|
122
122
|
return
|
|
123
|
-
current = info.get("num_ctx") or _ctx_cap.get(model) or
|
|
123
|
+
current = info.get("num_ctx") or _ctx_cap.get(model) or SERVER_NUM_CTX
|
|
124
124
|
lower = max(MIN_NUM_CTX, current // 2)
|
|
125
125
|
if lower < current:
|
|
126
126
|
_ctx_cap[model] = lower
|
|
@@ -215,11 +215,16 @@ async def current(num_ctx: int) -> dict:
|
|
|
215
215
|
return state
|
|
216
216
|
|
|
217
217
|
|
|
218
|
+
#: The context the server asks for unless told otherwise (its
|
|
219
|
+
#: VIRTUAI_LOCAL_NUM_CTX); what the fit report estimates at.
|
|
220
|
+
SERVER_NUM_CTX = 16_384
|
|
221
|
+
|
|
222
|
+
|
|
218
223
|
def _num_ctx() -> int:
|
|
219
224
|
try:
|
|
220
|
-
return int(os.environ.get("OLLAMA_NUM_CTX") or
|
|
225
|
+
return int(os.environ.get("OLLAMA_NUM_CTX") or SERVER_NUM_CTX)
|
|
221
226
|
except ValueError:
|
|
222
|
-
return
|
|
227
|
+
return SERVER_NUM_CTX
|
|
223
228
|
|
|
224
229
|
|
|
225
230
|
async def snapshot() -> dict:
|
|
@@ -303,9 +303,18 @@ class _Session:
|
|
|
303
303
|
continue
|
|
304
304
|
if sock.done.is_set():
|
|
305
305
|
break
|
|
306
|
+
# The beat's clock starts when it is sent, and a slow one gives up
|
|
307
|
+
# at two intervals: it used to wait out the client's 40 s read
|
|
308
|
+
# timeout, and the beats behind it waited too — on a laptop busy
|
|
309
|
+
# with a local model and a `find /`, long enough for the server
|
|
310
|
+
# to declare the job lost while it ran.
|
|
311
|
+
last_beat = time.monotonic()
|
|
306
312
|
try:
|
|
307
|
-
|
|
308
|
-
|
|
313
|
+
# wait_for bounds the whole request; httpx's timeout is per
|
|
314
|
+
# read, and a reply trickling in would get past it.
|
|
315
|
+
reply = await asyncio.wait_for(
|
|
316
|
+
self.http.post(f"{base}/heartbeat", json={"events": sock.take_events()}),
|
|
317
|
+
timeout=max(2.0, 2 * self.heartbeat))
|
|
309
318
|
if reply.status_code == 200 and reply.json().get("cancel"):
|
|
310
319
|
self.on_status("warn", f"Job {job_id} cancelled by the server")
|
|
311
320
|
if task is not None:
|
|
@@ -314,7 +323,7 @@ class _Session:
|
|
|
314
323
|
"v": 1, "id": job.get("id"), "type": "error", "error": "cancelled by the server"}
|
|
315
324
|
sock.done.set()
|
|
316
325
|
except Exception as exc:
|
|
317
|
-
logger.
|
|
326
|
+
logger.warning("Heartbeat for %s failed: %s", job_id, exc)
|
|
318
327
|
|
|
319
328
|
final = sock.final or runner._busy_reply(job, "the runner finished this request without a reply") or {
|
|
320
329
|
"v": 1, "id": job.get("id"), "type": "error", "error": "no reply"}
|
|
@@ -222,18 +222,20 @@ async def _handle_model_generate(frame: dict, ws, on_status: StatusCallback) ->
|
|
|
222
222
|
|
|
223
223
|
tool_calls = list(getattr(gathered, "tool_calls", []) or []) if gathered else []
|
|
224
224
|
usage = dict(getattr(gathered, "usage_metadata", None) or {}) if gathered else {}
|
|
225
|
-
|
|
226
|
-
"v": 1, "id": fid, "type": "model_result",
|
|
227
|
-
"tool_calls": tool_calls, "usage": usage,
|
|
228
|
-
}))
|
|
229
|
-
# After the reply, so the user never waits on it: what the model
|
|
230
|
-
# really took decides the context of its next call here.
|
|
225
|
+
result = {"v": 1, "id": fid, "type": "model_result", "tool_calls": tool_calls, "usage": usage}
|
|
231
226
|
if not omlx:
|
|
227
|
+
# What the model really took decides the context of its next call
|
|
228
|
+
# here, and the server hears it in this reply — not a minute later
|
|
229
|
+
# in a status report, by which time it may have sent a prompt this
|
|
230
|
+
# context would cut. A local /api/ps: milliseconds.
|
|
232
231
|
from virtuai_cli import ollama_state
|
|
233
232
|
try:
|
|
234
233
|
await asyncio.wait_for(ollama_state.after_call(model), timeout=5.0)
|
|
235
234
|
except Exception:
|
|
236
235
|
pass
|
|
236
|
+
if ollama_state.context_for(model, None):
|
|
237
|
+
result["context_cap"] = ollama_state.context_for(model, None)
|
|
238
|
+
await ws.send(json.dumps(result))
|
|
237
239
|
except Exception as exc:
|
|
238
240
|
logger.warning("model_generate failed: %s", exc)
|
|
239
241
|
await _error(str(exc))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: virtuai-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Run VirtuAI deep agents on your local machine
|
|
5
5
|
Author-email: uCloudStore <lmoreno@ucloudstore.com>
|
|
6
6
|
License: Proprietary
|
|
@@ -138,7 +138,7 @@ Each model call goes to the runner of the person the turn is for; if they have n
|
|
|
138
138
|
|
|
139
139
|
At connect and then every minute: the models it has, the Ollama version, what is loaded and how much memory it really took. With `llmfit` installed it also estimates which pulled models fit this machine, so the server does not send a model to a runner that cannot hold it until it has actually run there. A model that spills to the CPU gets half the context on its next call on that machine (down to 4k).
|
|
140
140
|
|
|
141
|
-
The context sent to Ollama is
|
|
141
|
+
The context sent to Ollama is 16k by default (set on the server). A model that does not fit at that context — `qwen3:14b` on a 16 GB Mac spills 7% to the CPU — is capped at half on that machine after its first call, and the server is told in the same reply: from then on it sends prompts that fit, compacting the conversation when they would not, instead of letting Ollama cut the start of the prompt. `OLLAMA_NUM_CTX` sets a lower context on a machine with little memory. For qwen3 the runner turns Ollama's thinking mode off: left on, it reasoned for minutes before every tool call.
|
|
142
142
|
|
|
143
143
|
### oMLX
|
|
144
144
|
|
|
@@ -163,7 +163,7 @@ mean "allow more commands at once" silently launched more browsers.
|
|
|
163
163
|
| `VIRTUAI_SANDBOX_OWNER` | unset | The one person this runner serves, reported to the server so a browser action reaches *their* browser. Leave it unset on a laptop or a container shared by a team: the server then routes to it for anyone, which is the behaviour that existed before per-person sandboxes. Set it when a workspace has a sandbox per person, or actions land on whichever runner connected last. |
|
|
164
164
|
| `VIRTUAI_SERVE_MODELS` | `own` | Whose local-model turns this runner answers: `own`, `workspace` or `none`. Also set by `--serve-models`. |
|
|
165
165
|
| `OLLAMA_BASE_URL` | `http://localhost:11434` | Where to reach the local Ollama. |
|
|
166
|
-
| `OLLAMA_NUM_CTX` | unset | The context to ask Ollama for, instead of the server's (
|
|
166
|
+
| `OLLAMA_NUM_CTX` | unset | The context to ask Ollama for, instead of the server's (16k). Lower it on a machine with little memory. |
|
|
167
167
|
| `VIRTUAI_OMLX_URL` | from `~/.omlx/settings.json`, else `http://127.0.0.1:8000` | Where to reach oMLX. |
|
|
168
168
|
| `OMLX_API_KEY` | from `~/.omlx/settings.json` | oMLX's API key. Stays on this machine. |
|
|
169
169
|
|
|
@@ -134,3 +134,18 @@ def test_the_runner_reports_whose_turns_it_serves(monkeypatch, env, expected):
|
|
|
134
134
|
payload = asyncio.run(R._ready_payload(Path("/tmp")))
|
|
135
135
|
assert payload["models"] == {"serve": expected}
|
|
136
136
|
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_a_model_that_spilled_reports_its_cap_in_the_same_reply(monkeypatch):
|
|
140
|
+
"""The server hears it on this call, not a minute later in a status report."""
|
|
141
|
+
from virtuai_cli import ollama_state
|
|
142
|
+
|
|
143
|
+
async def spilled(model):
|
|
144
|
+
ollama_state._ctx_cap[model] = 8192
|
|
145
|
+
|
|
146
|
+
monkeypatch.setattr(ollama_state, "after_call", spilled)
|
|
147
|
+
monkeypatch.setattr(ollama_state, "_ctx_cap", {})
|
|
148
|
+
built, sent = _run({**FRAME, "num_ctx": 16384}, monkeypatch)
|
|
149
|
+
assert built["num_ctx"] == 16384 and sent[-1]["context_cap"] == 8192
|
|
150
|
+
built, sent = _run({**FRAME, "num_ctx": 16384}, monkeypatch)
|
|
151
|
+
assert built["num_ctx"] == 8192, "and asks Ollama for less from then on"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|