@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.
@@ -1526,14 +1526,15 @@ async function runAgentChatJob(job) {
1526
1526
  emailFrom: payload?.emailFrom ?? "",
1527
1527
  emailAllowList: payload?.emailAllowList ?? "",
1528
1528
  });
1529
- // 10 min max for an agent run: 5 ReAct steps on a 14B with a cold prompt cache —
1530
- // or a remote Ollama box cold-loading a big model — can exceed the old caps. Must
1531
- // stay BELOW the web client's hard WS deadline (660s) so our clean timeout error
1532
- // reaches the user instead of a generic WS timeout. On expiry the python child is
1533
- // SIGKILLed, which also shows up as a BrokenPipeError in the model server log
1534
- // (expected). Liveness during long model calls comes from the python heartbeat
1535
- // (a keepalive step chunk every 45s), not from this cap.
1536
- const timeoutMs = 600_000;
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 = "";
@@ -369,12 +369,16 @@ def _web_search_impl(query):
369
369
  )
370
370
 
371
371
 
372
- def _detect_model_id(base_url, api_key=""):
373
- """Ask an OpenAI-compatible server which model it serves.
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
- Queries GET {base_url}/models and returns the first reported model id.
376
- `base_url` already ends with /v1. Returns "" on any failure so callers can
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 detect failed {url}: {e}")
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) and data:
392
- ids = [d["id"].strip() for d in data
393
- if isinstance(d, dict) and isinstance(d.get("id"), str) and d["id"].strip()]
394
- if ids:
395
- if len(ids) > 1:
396
- # Ollama servers list every pulled model; "first" is arbitrary and may
397
- # be a huge/slow one (seen live: picked gemma4:31b over qwen3:14b).
398
- _log(f"model detect: server hosts {len(ids)} models {ids} using "
399
- f"{ids[0]!r}. Set the coding model NAME in Settings to choose "
400
- "a specific one.")
401
- return ids[0]
402
- return ""
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 240s so one hung request (remote server
1928
- # cold-loading a big model) fails into OUR retry loop instead of eating
1929
- # the whole worker budget; by the retry the model is usually loaded.
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": 240.0, "max_retries": 0},
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.136",
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",