@tiens.nguyen/gonext-local-worker 1.0.136 → 1.0.140
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.
- package/gonext-local-worker.mjs +9 -8
- package/gonext_agent_chat.py +97 -23
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -1526,14 +1526,15 @@ async function runAgentChatJob(job) {
|
|
|
1526
1526
|
emailFrom: payload?.emailFrom ?? "",
|
|
1527
1527
|
emailAllowList: payload?.emailAllowList ?? "",
|
|
1528
1528
|
});
|
|
1529
|
-
//
|
|
1530
|
-
// or a remote Ollama box cold-loading a big model — can
|
|
1531
|
-
// stay BELOW the web client's hard WS deadline
|
|
1532
|
-
// reaches the user instead of a generic
|
|
1533
|
-
// SIGKILLed, which also shows up as a
|
|
1534
|
-
// (expected). Liveness during long model
|
|
1535
|
-
// (a keepalive step chunk every 45s), not
|
|
1536
|
-
|
|
1529
|
+
// 30 min max for an agent run: multi-step ReAct on a 14B/31B with a cold prompt
|
|
1530
|
+
// cache — or a remote Ollama box on slow GPU cold-loading a big model — can run
|
|
1531
|
+
// for many minutes per step. Must stay BELOW the web client's hard WS deadline
|
|
1532
|
+
// (1_860_000 ms) so our clean timeout error reaches the user instead of a generic
|
|
1533
|
+
// WS timeout. On expiry the python child is SIGKILLed, which also shows up as a
|
|
1534
|
+
// BrokenPipeError in the model server log (expected). Liveness during long model
|
|
1535
|
+
// calls comes from the python heartbeat (a keepalive step chunk every 45s), not
|
|
1536
|
+
// from this cap.
|
|
1537
|
+
const timeoutMs = 1_800_000;
|
|
1537
1538
|
|
|
1538
1539
|
let inThink = false;
|
|
1539
1540
|
let finalText = "";
|
package/gonext_agent_chat.py
CHANGED
|
@@ -369,12 +369,16 @@ def _web_search_impl(query):
|
|
|
369
369
|
)
|
|
370
370
|
|
|
371
371
|
|
|
372
|
-
|
|
373
|
-
"""
|
|
372
|
+
class _AgentConfigError(RuntimeError):
|
|
373
|
+
"""Deterministic setup error (e.g. wrong model name/URL). Not retryable and not
|
|
374
|
+
worth a plain-reply degrade — the user needs the message itself to fix Settings."""
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _list_model_ids(base_url, api_key=""):
|
|
378
|
+
"""Return the model ids an OpenAI-compatible server reports at {base_url}/models.
|
|
374
379
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
fall back. Used when the user supplies a coding-model URL but no model name.
|
|
380
|
+
`base_url` already ends with /v1. Returns [] on any failure so callers can
|
|
381
|
+
fall back.
|
|
378
382
|
"""
|
|
379
383
|
url = base_url.rstrip("/") + "/models"
|
|
380
384
|
headers = {"Accept": "application/json"}
|
|
@@ -385,21 +389,49 @@ def _detect_model_id(base_url, api_key=""):
|
|
|
385
389
|
with urllib.request.urlopen(req, timeout=10, context=_ssl_context()) as resp:
|
|
386
390
|
payload = json.loads(resp.read().decode("utf-8", errors="replace"))
|
|
387
391
|
except Exception as e: # noqa: BLE001
|
|
388
|
-
_log(f"model
|
|
389
|
-
return
|
|
392
|
+
_log(f"model list failed {url}: {e}")
|
|
393
|
+
return []
|
|
390
394
|
data = payload.get("data") if isinstance(payload, dict) else None
|
|
391
|
-
if isinstance(data, list)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
395
|
+
if not isinstance(data, list):
|
|
396
|
+
return []
|
|
397
|
+
return [d["id"].strip() for d in data
|
|
398
|
+
if isinstance(d, dict) and isinstance(d.get("id"), str) and d["id"].strip()]
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _is_ollama_server(base_url):
|
|
402
|
+
"""True if the OpenAI-compatible base URL is an Ollama server.
|
|
403
|
+
|
|
404
|
+
Ollama's root path answers GET / with the plain-text banner "Ollama is
|
|
405
|
+
running". Used to gate Ollama-only request params (reasoning_effort) that an
|
|
406
|
+
MLX server might reject.
|
|
407
|
+
"""
|
|
408
|
+
root = re.sub(r"/v1/?$", "", (base_url or "").rstrip("/"))
|
|
409
|
+
if not root:
|
|
410
|
+
return False
|
|
411
|
+
req = urllib.request.Request(root, headers={"User-Agent": "gonext-agent/1.0"})
|
|
412
|
+
try:
|
|
413
|
+
with urllib.request.urlopen(req, timeout=6, context=_ssl_context()) as resp:
|
|
414
|
+
return b"ollama" in resp.read(200).lower()
|
|
415
|
+
except Exception: # noqa: BLE001
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _detect_model_id(base_url, api_key=""):
|
|
420
|
+
"""Ask an OpenAI-compatible server which model it serves (first reported id).
|
|
421
|
+
|
|
422
|
+
Returns "" on any failure so callers can fall back. Used when the user supplies
|
|
423
|
+
a coding-model URL but no model name.
|
|
424
|
+
"""
|
|
425
|
+
ids = _list_model_ids(base_url, api_key)
|
|
426
|
+
if not ids:
|
|
427
|
+
return ""
|
|
428
|
+
if len(ids) > 1:
|
|
429
|
+
# Ollama servers list every pulled model; "first" is arbitrary and may be a
|
|
430
|
+
# huge/slow one (seen live: picked gemma4:31b over qwen3:14b).
|
|
431
|
+
_log(f"model detect: server hosts {len(ids)} models {ids} — using "
|
|
432
|
+
f"{ids[0]!r}. Set the coding model NAME in Settings to choose "
|
|
433
|
+
"a specific one.")
|
|
434
|
+
return ids[0]
|
|
403
435
|
|
|
404
436
|
|
|
405
437
|
def _summarise_step(step_log):
|
|
@@ -1782,6 +1814,20 @@ def run_agent_chat(cfg):
|
|
|
1782
1814
|
msg = super().generate(*args, **kwargs)
|
|
1783
1815
|
break
|
|
1784
1816
|
except Exception as e: # noqa: BLE001
|
|
1817
|
+
emsg = str(e)
|
|
1818
|
+
# A 404 "model not found" is a config error (wrong model NAME
|
|
1819
|
+
# for this server, e.g. an MLX-style name against Ollama) —
|
|
1820
|
+
# deterministic, so retrying is pointless. Fail fast with the
|
|
1821
|
+
# server's actual model list so the user can fix Settings.
|
|
1822
|
+
if "404" in emsg and "not found" in emsg.lower():
|
|
1823
|
+
ids = _list_model_ids(coding_base_url, agent_api_key)
|
|
1824
|
+
have = f" This server has: {', '.join(ids)}." if ids else ""
|
|
1825
|
+
raise _AgentConfigError(
|
|
1826
|
+
f"The agent coding model {coding_model_id!r} does not "
|
|
1827
|
+
f"exist on {coding_base_url}.{have} Set 'Agent coding "
|
|
1828
|
+
"model name' in Settings → Agent to one of these exact "
|
|
1829
|
+
"names (for Ollama use the tag, e.g. qwen3:14b)."
|
|
1830
|
+
) from e
|
|
1785
1831
|
last_err = e
|
|
1786
1832
|
if attempt < 2:
|
|
1787
1833
|
_log(f"generate transient error (attempt {attempt + 1}/3): {e} — retrying")
|
|
@@ -1920,16 +1966,27 @@ def run_agent_chat(cfg):
|
|
|
1920
1966
|
return ChatMessage(role=MessageRole.ASSISTANT, content=text)
|
|
1921
1967
|
|
|
1922
1968
|
try:
|
|
1969
|
+
# Ollama's OpenAI-compat endpoint ignores Qwen3's /no_think soft-switch (its
|
|
1970
|
+
# template forces thinking mode; "think": false is also ignored there) but DOES
|
|
1971
|
+
# honor reasoning_effort="none". Thinking burned ~430 of 487 output tokens in
|
|
1972
|
+
# the live test — minutes per step on a slow GPU — so disable it explicitly.
|
|
1973
|
+
# Sniffed per-server because MLX might reject the unknown param.
|
|
1974
|
+
extra_model_kwargs = {}
|
|
1975
|
+
if _is_ollama_server(coding_base_url):
|
|
1976
|
+
_log("coding server is Ollama → sending reasoning_effort='none' to disable thinking")
|
|
1977
|
+
extra_model_kwargs["reasoning_effort"] = "none"
|
|
1923
1978
|
model = _LoggingModel(
|
|
1924
1979
|
model_id=coding_model_id,
|
|
1925
1980
|
api_base=coding_base_url,
|
|
1926
1981
|
api_key=agent_api_key,
|
|
1927
|
-
# Cap each HTTP attempt at
|
|
1928
|
-
#
|
|
1929
|
-
# the
|
|
1982
|
+
# Cap each HTTP attempt at 600s so a genuinely slow single call (a
|
|
1983
|
+
# remote M6000/Vulkan box doing a cold ~4-5min prompt-eval) can finish
|
|
1984
|
+
# on the first try, while a truly hung request still fails into OUR
|
|
1985
|
+
# retry loop rather than eating the whole 30min worker budget.
|
|
1930
1986
|
# max_retries=0: the openai client's own silent retries would otherwise
|
|
1931
1987
|
# stack multiplicatively with ours (3 × 3 attempts).
|
|
1932
|
-
client_kwargs={"timeout":
|
|
1988
|
+
client_kwargs={"timeout": 600.0, "max_retries": 0},
|
|
1989
|
+
**extra_model_kwargs,
|
|
1933
1990
|
)
|
|
1934
1991
|
agent_tools = [http_request, web_search, fetch_url, calculate,
|
|
1935
1992
|
get_current_datetime, create_pdf]
|
|
@@ -1963,6 +2020,23 @@ def run_agent_chat(cfg):
|
|
|
1963
2020
|
_log(f"done (deterministic, no summarizer call): {len(final_text)} chars")
|
|
1964
2021
|
_emit({"type": "final", "text": final_text})
|
|
1965
2022
|
except Exception as e: # noqa: BLE001
|
|
2023
|
+
# Config errors (wrong coding-model name/URL) must reach the user VERBATIM —
|
|
2024
|
+
# they contain the fix (the server's real model list). smolagents wraps model
|
|
2025
|
+
# exceptions in AgentGenerationError, so walk the cause chain to find ours.
|
|
2026
|
+
cfg_err = None
|
|
2027
|
+
cur, depth = e, 0
|
|
2028
|
+
while cur is not None and depth < 6:
|
|
2029
|
+
if isinstance(cur, _AgentConfigError):
|
|
2030
|
+
cfg_err = cur
|
|
2031
|
+
break
|
|
2032
|
+
cur, depth = (getattr(cur, "__cause__", None)
|
|
2033
|
+
or getattr(cur, "__context__", None)), depth + 1
|
|
2034
|
+
if cfg_err is None and "Settings → Agent" in str(e):
|
|
2035
|
+
cfg_err = e # wrapped without a cause chain; message survived
|
|
2036
|
+
if cfg_err is not None:
|
|
2037
|
+
_log(f"agent config error (not degrading): {cfg_err}")
|
|
2038
|
+
_emit({"type": "final", "text": f"⚠️ {cfg_err}"})
|
|
2039
|
+
return
|
|
1966
2040
|
# An unexpected failure inside the agent loop should still return a useful
|
|
1967
2041
|
# answer from the conversation rather than surfacing a raw error to the user.
|
|
1968
2042
|
_log(f"agent error: {e} — degrading to plain reply")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gonext-local-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.140",
|
|
4
4
|
"description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|