@tiens.nguyen/gonext-local-worker 1.0.330 → 1.0.333
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 +104 -15
- package/gonext-repl.mjs +36 -11
- package/gonext_agent_chat.py +138 -37
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -1768,6 +1768,75 @@ function _warmupPing(quiet) {
|
|
|
1768
1768
|
});
|
|
1769
1769
|
}
|
|
1770
1770
|
|
|
1771
|
+
// ─── Coding-backend resolution (task #117, folded into #123 Part B) ──────────────────
|
|
1772
|
+
// ONE answer to "which coding backend is this job using?", for the two decisions THIS
|
|
1773
|
+
// process makes before python starts: keep an Ollama model warm, and fetch the
|
|
1774
|
+
// OpenAI-compatible API key. The answer is passed down as `codingBackend` so python
|
|
1775
|
+
// doesn't re-derive it and can't disagree.
|
|
1776
|
+
//
|
|
1777
|
+
// The reference implementation is _resolve_coding_backend() in gonext_agent_chat.py —
|
|
1778
|
+
// it is pure and carries the offline resolution table (tests/test_backend_resolution.py).
|
|
1779
|
+
// Keep this rule in step with it: explicit kind wins → Ollama probe → remote ⇒ openai →
|
|
1780
|
+
// local. The probe runs BEFORE the remote rule so a remote Ollama box stays "ollama".
|
|
1781
|
+
|
|
1782
|
+
// root → { value, until }. A YES is cached long (a box doesn't stop being Ollama), a NO
|
|
1783
|
+
// only briefly: "not Ollama" and "the probe failed" are indistinguishable here, so caching
|
|
1784
|
+
// a failure would strand a box that was merely booting/unreachable for one job.
|
|
1785
|
+
const _ollamaProbeCache = new Map();
|
|
1786
|
+
const _PROBE_TTL_YES_MS = 30 * 60_000;
|
|
1787
|
+
const _PROBE_TTL_NO_MS = 60_000;
|
|
1788
|
+
|
|
1789
|
+
async function _probeIsOllama(baseURL) {
|
|
1790
|
+
const root = String(baseURL || "").replace(/\/+$/, "").replace(/\/v1$/i, "");
|
|
1791
|
+
if (!root) return false;
|
|
1792
|
+
const hit = _ollamaProbeCache.get(root);
|
|
1793
|
+
if (hit && hit.until > Date.now()) return hit.value;
|
|
1794
|
+
let isOllama = false;
|
|
1795
|
+
try {
|
|
1796
|
+
// Ollama answers GET / with the plain-text banner "Ollama is running".
|
|
1797
|
+
const res = await fetch(root, {
|
|
1798
|
+
headers: { "User-Agent": "gonext-worker/1.0" },
|
|
1799
|
+
signal: AbortSignal.timeout(6000),
|
|
1800
|
+
});
|
|
1801
|
+
isOllama = res.ok && /ollama/i.test((await res.text()).slice(0, 200));
|
|
1802
|
+
} catch {
|
|
1803
|
+
isOllama = false; // unreachable/TLS/404 → not provably Ollama
|
|
1804
|
+
}
|
|
1805
|
+
_ollamaProbeCache.set(root, {
|
|
1806
|
+
value: isOllama,
|
|
1807
|
+
until: Date.now() + (isOllama ? _PROBE_TTL_YES_MS : _PROBE_TTL_NO_MS),
|
|
1808
|
+
});
|
|
1809
|
+
return isOllama;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function _isLocalCodingUrl(baseURL) {
|
|
1813
|
+
let host = "";
|
|
1814
|
+
try {
|
|
1815
|
+
host = new URL(String(baseURL || "")).hostname.toLowerCase().replace(/\.$/, "");
|
|
1816
|
+
} catch {
|
|
1817
|
+
return true; // unparseable → we are certainly not talking to a paid remote
|
|
1818
|
+
}
|
|
1819
|
+
if (!host) return true;
|
|
1820
|
+
if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
|
|
1821
|
+
if (["localhost", "0.0.0.0", "::", "::1"].includes(host)) return true;
|
|
1822
|
+
if (/^127(\.\d{1,3}){3}$/.test(host)) return true;
|
|
1823
|
+
if (/^(10(\.\d{1,3}){3}|192\.168(\.\d{1,3}){2}|172\.(1[6-9]|2\d|3[01])(\.\d{1,3}){2}|169\.254(\.\d{1,3}){2})$/.test(host))
|
|
1824
|
+
return true;
|
|
1825
|
+
if (host.includes(":") && (host.startsWith("fe80:") || /^f[cd]/.test(host))) return true;
|
|
1826
|
+
if (/\.(local|localdomain|lan|home|internal)$/.test(host)) return true;
|
|
1827
|
+
// A dotless hostname ("mac-studio", "ollama1") is a LAN box: a public host always has a
|
|
1828
|
+
// dot, and erring this way only ever means "don't expect an API key here".
|
|
1829
|
+
return !host.includes(".") && !host.includes(":");
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
/** Resolve to "ollama" | "openai" | "local". `kind` is the user's Settings choice
|
|
1833
|
+
* ("ollama" | "openai" | "" = Auto). Only Auto costs a probe. */
|
|
1834
|
+
async function resolveCodingBackend(kind, baseURL) {
|
|
1835
|
+
if (kind === "ollama" || kind === "openai" || kind === "local") return kind;
|
|
1836
|
+
if (await _probeIsOllama(baseURL)) return "ollama";
|
|
1837
|
+
return _isLocalCodingUrl(baseURL) ? "local" : "openai";
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1771
1840
|
/**
|
|
1772
1841
|
* Remember the agent's Ollama coding model (from a claimed agent_chat job) and keep it
|
|
1773
1842
|
* resident via a background interval. Warms IMMEDIATELY only when the model is first
|
|
@@ -1801,19 +1870,34 @@ async function runAgentChatJob(job) {
|
|
|
1801
1870
|
// "cancelled" status with completed/failed.
|
|
1802
1871
|
const cancelAc = new AbortController();
|
|
1803
1872
|
let cancelled = false;
|
|
1804
|
-
//
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
// that endpoint doesn't exist there and we shouldn't hit a paid API with keep-warm pings.
|
|
1809
|
-
if (payload?.codingKind !== "openai") {
|
|
1810
|
-
trackCodingModelForWarmup(payload?.codingBaseURL ?? "", payload?.codingModelId ?? "");
|
|
1811
|
-
}
|
|
1873
|
+
// Task #108: the user's Coding backend setting ("ollama" | "openai" | "" = Auto).
|
|
1874
|
+
const codingKind = ["ollama", "openai", "local"].includes(payload?.codingKind)
|
|
1875
|
+
? payload.codingKind
|
|
1876
|
+
: "";
|
|
1812
1877
|
const runRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
1813
1878
|
method: "PATCH",
|
|
1814
1879
|
body: JSON.stringify({ jobStatus: "running" }),
|
|
1815
1880
|
});
|
|
1816
1881
|
await ensureWorkerOk(runRes, `mark running agent_chat jobId=${jobId}`);
|
|
1882
|
+
// Task #117/#123: `codingKind` above is the SETTING, not the answer — resolve the actual
|
|
1883
|
+
// backend ONCE here and let both decisions below, plus python, read that one value.
|
|
1884
|
+
// AFTER the "running" PATCH on purpose: an Auto config costs a probe (~6s worst case,
|
|
1885
|
+
// then cached), and the REPL shouldn't watch a claimed job sit in "queued" for it.
|
|
1886
|
+
const codingBackend = await resolveCodingBackend(codingKind, payload?.codingBaseURL ?? "");
|
|
1887
|
+
console.log(
|
|
1888
|
+
`[gonext-worker] agent job ${jobId}: coding backend ${codingBackend}` +
|
|
1889
|
+
` (kind=${codingKind || "auto"}, ${payload?.codingBaseURL || "no coding url"})`
|
|
1890
|
+
);
|
|
1891
|
+
// Remember this job's Ollama coding model and keep it resident via a background
|
|
1892
|
+
// interval. Only warms now if the model is new/changed — repeat questions rely on
|
|
1893
|
+
// the interval, so we don't re-warm (or re-log) on every question. The warm-up uses
|
|
1894
|
+
// Ollama's native /api/generate, so it runs ONLY for an actual Ollama backend: on
|
|
1895
|
+
// anything else that endpoint doesn't exist, and on a cloud one we'd be pinging a paid
|
|
1896
|
+
// API every 4 minutes (which an Auto+cloud config did until #117 was fixed — the old
|
|
1897
|
+
// gate only skipped an EXPLICIT openai kind).
|
|
1898
|
+
if (codingBackend === "ollama") {
|
|
1899
|
+
trackCodingModelForWarmup(payload?.codingBaseURL ?? "", payload?.codingModelId ?? "");
|
|
1900
|
+
}
|
|
1817
1901
|
|
|
1818
1902
|
let buf = "";
|
|
1819
1903
|
let flushTimer = null;
|
|
@@ -1902,14 +1986,14 @@ async function runAgentChatJob(job) {
|
|
|
1902
1986
|
jobActiveWorkspace = webWork;
|
|
1903
1987
|
}
|
|
1904
1988
|
|
|
1905
|
-
// Task #108:
|
|
1906
|
-
//
|
|
1907
|
-
//
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1989
|
+
// Task #108: for an OpenAI-compatible coder, fetch the API key from a dedicated
|
|
1990
|
+
// worker-authed endpoint (NOT the job payload/Mongo doc — secret hygiene) and inject
|
|
1991
|
+
// it into python stdin only. Keyed off the RESOLVED backend (#117), so an Auto config
|
|
1992
|
+
// pointing at a cloud endpoint now gets its key: it used to require the explicit
|
|
1993
|
+
// "openai" kind, and python's `coding_api_key or agent_api_key` fallback then
|
|
1994
|
+
// authenticated a cloud provider with the LOCAL agent key → 401.
|
|
1911
1995
|
let codingApiKey = "";
|
|
1912
|
-
if (
|
|
1996
|
+
if (codingBackend === "openai") {
|
|
1913
1997
|
try {
|
|
1914
1998
|
const kr = await workerFetch("/api/worker/coding-key");
|
|
1915
1999
|
if (kr.ok) {
|
|
@@ -1929,6 +2013,11 @@ async function runAgentChatJob(job) {
|
|
|
1929
2013
|
codingBaseURL: payload?.codingBaseURL ?? "",
|
|
1930
2014
|
codingModelId: payload?.codingModelId ?? "",
|
|
1931
2015
|
codingKind,
|
|
2016
|
+
// The resolved backend (#117): python trusts this instead of probing again, so the
|
|
2017
|
+
// key fetch above and python's streaming / repair / step-budget branches can't
|
|
2018
|
+
// disagree about what this endpoint is. Python re-resolves only if it ends up on a
|
|
2019
|
+
// DIFFERENT url than the one judged here (its no-model-id fallback to the chat model).
|
|
2020
|
+
codingBackend,
|
|
1932
2021
|
codingApiKey,
|
|
1933
2022
|
// Task #107: when true, python emits a {type:code_response} event per code-model
|
|
1934
2023
|
// call so the worker persists the full raw text to Mongo.
|
package/gonext-repl.mjs
CHANGED
|
@@ -876,32 +876,52 @@ async function chooseModel() {
|
|
|
876
876
|
}
|
|
877
877
|
const allowed = Array.isArray(probe?.codingAllowed) ? probe.codingAllowed : [];
|
|
878
878
|
const def = (probe?.codingDefault ?? "").trim();
|
|
879
|
-
|
|
879
|
+
// Task #123: a choice is a (backend, model) PAIR — several backends (Ollama, an
|
|
880
|
+
// OpenAI-compatible endpoint, local MLX) can be enabled at once, and picking one moves
|
|
881
|
+
// the URL, the kind and the API key with it. `codingChoices` carries the backend of each
|
|
882
|
+
// entry so the list can say which is which; `codingAllowed` is the same ids, and stays
|
|
883
|
+
// the fallback for an API that predates this.
|
|
884
|
+
const choices = Array.isArray(probe?.codingChoices) && probe.codingChoices.length
|
|
885
|
+
? probe.codingChoices
|
|
886
|
+
: allowed.map((id) => ({ id, model: id, kind: "" }));
|
|
887
|
+
if (choices.length === 0) {
|
|
880
888
|
console.log(dim(" No coding model configured. Set one in the web app → Settings → Agent.\n"));
|
|
881
889
|
return;
|
|
882
890
|
}
|
|
883
|
-
if (
|
|
891
|
+
if (choices.length === 1) {
|
|
884
892
|
console.log(
|
|
885
|
-
dim(` Only one coding model is available: ${
|
|
886
|
-
dim(" Add more in the web app → Settings → Agent →
|
|
893
|
+
dim(` Only one coding model is available: ${choices[0].model}.\n`) +
|
|
894
|
+
dim(" Add more in the web app → Settings → Agent → Coding backends.\n")
|
|
887
895
|
);
|
|
888
896
|
return;
|
|
889
897
|
}
|
|
890
898
|
const active = sessionCodingModel || def;
|
|
891
|
-
// Arrow-key picker (↑/↓ + Enter) — pre-highlight the active model, tag the default
|
|
892
|
-
|
|
893
|
-
const
|
|
899
|
+
// Arrow-key picker (↑/↓ + Enter) — pre-highlight the active model, tag the default, and
|
|
900
|
+
// name the backend when more than one is in play (the same model name can appear twice).
|
|
901
|
+
const KIND_LABEL = { ollama: "Ollama", openai: "OpenAI-compatible", local: "local MLX", "": "auto" };
|
|
902
|
+
const showKind = new Set(choices.map((c) => c.kind ?? "")).size > 1;
|
|
903
|
+
const labels = choices.map((c) => {
|
|
904
|
+
const kind = showKind ? ` ${dim(`· ${KIND_LABEL[c.kind ?? ""] ?? c.kind}`)}` : "";
|
|
905
|
+
return `${c.model}${kind}${c.id === def ? " (default)" : ""}`;
|
|
906
|
+
});
|
|
907
|
+
const activeIdx = choices.findIndex((c) => c.id === active);
|
|
894
908
|
console.log(dim(" Choose the coding model (↑/↓ then Enter):"));
|
|
895
909
|
const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
|
|
896
910
|
if (idx < 0) {
|
|
897
911
|
console.log(dim(" (cancelled)\n"));
|
|
898
912
|
return;
|
|
899
913
|
}
|
|
900
|
-
const chosen =
|
|
914
|
+
const chosen = choices[idx];
|
|
901
915
|
// Empty override means "use the account default" — so don't send an override when the
|
|
902
916
|
// user picks the default (keeps the payload clean and lets a later default change win).
|
|
903
|
-
sessionCodingModel = chosen === def ? "" : chosen;
|
|
904
|
-
|
|
917
|
+
sessionCodingModel = chosen.id === def ? "" : chosen.id;
|
|
918
|
+
// Remember the friendly name for the token footer, which should never show a raw
|
|
919
|
+
// "<kind>::<model>" wire id.
|
|
920
|
+
sessionCodingLabel = chosen.id === def ? "" : chosen.model;
|
|
921
|
+
const where = showKind ? ` (${KIND_LABEL[chosen.kind ?? ""] ?? chosen.kind})` : "";
|
|
922
|
+
console.log(
|
|
923
|
+
green(` ✓ coding model → ${chosen.model}${where}${chosen.id === def ? " (default)" : ""}\n`)
|
|
924
|
+
);
|
|
905
925
|
}
|
|
906
926
|
|
|
907
927
|
// Probe whether SSH KEY auth already works for a server (so we can tell the user to run
|
|
@@ -1252,7 +1272,12 @@ let jobIdForApprovalRace = "";
|
|
|
1252
1272
|
|
|
1253
1273
|
// Per-session coding-model override chosen via /model (empty = use the account default).
|
|
1254
1274
|
// Sent to /agent-ask as codingModelOverride; the API validates it against the whitelist.
|
|
1275
|
+
// Task #123: with several backends configured this is a QUALIFIED id ("<kind>::<model>"),
|
|
1276
|
+
// which resolves to a whole backend — URL, kind and API key — not just a model name.
|
|
1255
1277
|
let sessionCodingModel = "";
|
|
1278
|
+
// The friendly model name behind sessionCodingModel, for display only: the token footer
|
|
1279
|
+
// must never show a raw "<kind>::<model>" wire id. Empty = running the account default.
|
|
1280
|
+
let sessionCodingLabel = "";
|
|
1256
1281
|
// The default coding model id (from the startup agent-payload probe) — shown in the
|
|
1257
1282
|
// per-turn token footer next to the GLOBAL output total so it's clear which model that
|
|
1258
1283
|
// lifetime count belongs to. The /model override (sessionCodingModel) wins when set.
|
|
@@ -2383,7 +2408,7 @@ async function main() {
|
|
|
2383
2408
|
// name so it's clear which coder that running total belongs to. Guard on THIS turn's
|
|
2384
2409
|
// activity so a plain-reply greeting that did no code-model work shows nothing (#112).
|
|
2385
2410
|
if ((inputTokens ?? 0) > 0 || (turnOutputTokens ?? 0) > 0) {
|
|
2386
|
-
const coder =
|
|
2411
|
+
const coder = sessionCodingLabel || defaultCodingModel;
|
|
2387
2412
|
console.log(
|
|
2388
2413
|
dim("tokens · ↑ in ") +
|
|
2389
2414
|
fmtTokens(inputTokens ?? 0) +
|
package/gonext_agent_chat.py
CHANGED
|
@@ -879,6 +879,110 @@ def _is_ollama_server(base_url):
|
|
|
879
879
|
return False
|
|
880
880
|
|
|
881
881
|
|
|
882
|
+
# Loopback and RFC1918/link-local ranges — a URL on one of these is a box the user owns,
|
|
883
|
+
# which is the whole point: only a REMOTE endpoint can plausibly need an API key.
|
|
884
|
+
_LOOPBACK_RE = re.compile(r"^127(?:\.\d{1,3}){3}$")
|
|
885
|
+
_PRIVATE_V4_RE = re.compile(
|
|
886
|
+
r"^(?:10(?:\.\d{1,3}){3}"
|
|
887
|
+
r"|192\.168(?:\.\d{1,3}){2}"
|
|
888
|
+
r"|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}"
|
|
889
|
+
r"|169\.254(?:\.\d{1,3}){2})$"
|
|
890
|
+
)
|
|
891
|
+
# mDNS / router-assigned search domains. A bare dotless hostname counts too (see below).
|
|
892
|
+
_LOCAL_SUFFIXES = (".local", ".localdomain", ".lan", ".home", ".internal")
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _is_local_url(base_url) -> bool:
|
|
896
|
+
"""True when the base URL points at this machine or the local network.
|
|
897
|
+
|
|
898
|
+
Kept pure (no I/O) so the backend resolution table can be replayed offline. A dotless
|
|
899
|
+
hostname ("mac-studio", "ollama1") is treated as LAN: a public host effectively always
|
|
900
|
+
has a dot, and being wrong in that direction is the safe way — a LAN box is simply
|
|
901
|
+
never told to expect an API key.
|
|
902
|
+
"""
|
|
903
|
+
host = _host_of(base_url)
|
|
904
|
+
if "@" in host:
|
|
905
|
+
host = host.rsplit("@", 1)[1] # strip user:pass@
|
|
906
|
+
if host.startswith("["):
|
|
907
|
+
host = host[1:].split("]", 1)[0] # [::1]:8080 → ::1
|
|
908
|
+
elif host.count(":") == 1:
|
|
909
|
+
host = host.rsplit(":", 1)[0] # host:port → host
|
|
910
|
+
host = host.strip().lower().rstrip(".")
|
|
911
|
+
if not host:
|
|
912
|
+
return True # no host at all → we are not talking to anyone remote
|
|
913
|
+
if host in ("localhost", "0.0.0.0", "::", "::1"):
|
|
914
|
+
return True
|
|
915
|
+
if _LOOPBACK_RE.match(host) or _PRIVATE_V4_RE.match(host):
|
|
916
|
+
return True
|
|
917
|
+
if ":" in host and (host.startswith("fe80:") or host[:2] in ("fc", "fd")):
|
|
918
|
+
return True # IPv6 link-local / unique-local
|
|
919
|
+
if host.endswith(_LOCAL_SUFFIXES):
|
|
920
|
+
return True
|
|
921
|
+
return "." not in host and ":" not in host
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def _resolve_coding_backend(kind, base_url, probe=None):
|
|
925
|
+
"""Answer "which coding backend is this?" ONCE. Returns (backend, why) where backend
|
|
926
|
+
is one of "ollama" | "openai" | "local" and `why` is log copy explaining the choice.
|
|
927
|
+
|
|
928
|
+
Task #117 (folded into #123). This used to be re-derived in three places from TWO
|
|
929
|
+
different questions — `coding_is_ollama` ("is this an Ollama server?", explicit kind
|
|
930
|
+
else a URL sniff) and `coding_kind == "openai"` ("did the user explicitly pick
|
|
931
|
+
OpenAI-compatible?") — and Auto fell between them: a cloud endpoint left on the
|
|
932
|
+
default Auto sniffed as not-Ollama and was not explicitly openai, so it got NEITHER
|
|
933
|
+
treatment (no streaming, none of the #113 reasoning repairs, and the small local step
|
|
934
|
+
budget). Every branch now keys off this one value.
|
|
935
|
+
|
|
936
|
+
Order matters. The Ollama probe runs BEFORE the remote-host rule so a remote Ollama
|
|
937
|
+
(ollama1.gomarsic.cc) still resolves to "ollama" and keeps its own treatment.
|
|
938
|
+
|
|
939
|
+
explicit kind → that value
|
|
940
|
+
probe says Ollama → "ollama"
|
|
941
|
+
remote URL (not ours) → "openai" ← only a remote endpoint needs a key
|
|
942
|
+
otherwise → "local" (an MLX box on 127.0.0.1)
|
|
943
|
+
|
|
944
|
+
Deliberately NOT keyed off "is an API key set": the Settings UI only renders the key
|
|
945
|
+
field once the OpenAI-compatible kind is picked, so on Auto the user cannot supply one
|
|
946
|
+
— a stored key there is only ever a leftover from switching the dropdown back, and
|
|
947
|
+
using it as the test would make the answer depend on that history.
|
|
948
|
+
|
|
949
|
+
`probe` is injectable so the resolution table can be replayed offline without network.
|
|
950
|
+
"""
|
|
951
|
+
k = (kind or "").strip().lower()
|
|
952
|
+
if k in ("ollama", "openai", "local"):
|
|
953
|
+
return k, "explicit setting"
|
|
954
|
+
if (probe or _is_ollama_server)(base_url):
|
|
955
|
+
return "ollama", "auto: server answers as Ollama"
|
|
956
|
+
if not _is_local_url(base_url):
|
|
957
|
+
return "openai", "auto: remote URL, not Ollama"
|
|
958
|
+
return "local", "auto: local URL, not Ollama"
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def _coding_backend_flags(backend):
|
|
962
|
+
"""Everything the resolved backend decides, derived in ONE place (task #117 → #123).
|
|
963
|
+
|
|
964
|
+
Pure, so the whole resolution → behaviour table can be replayed offline. The callers
|
|
965
|
+
below must not re-derive any of this from `coding_kind` or a URL sniff — that drift is
|
|
966
|
+
the bug this fixes.
|
|
967
|
+
|
|
968
|
+
ollama_tweaks — reasoning_effort='none' (Ollama's OpenAI-compat endpoint ignores
|
|
969
|
+
Qwen3's /no_think but honors this) and the worker's keep-warm ping.
|
|
970
|
+
stream — Ollama and remote OpenAI-compatible endpoints stream, so a long
|
|
971
|
+
generation can't trip a proxy idle read-timeout and include_usage
|
|
972
|
+
keeps output-token counting alive. Local MLX stays non-streaming.
|
|
973
|
+
openai_repairs— the #113 reasoning-model repairs (prose-only nudge, parse-error
|
|
974
|
+
sanitizing, no-code streak breaker). Never applied to Ollama/MLX.
|
|
975
|
+
budget — (default, workspace) step budget: a cloud coder is stronger and
|
|
976
|
+
cheaper-per-step than a small local model, so it gets more room.
|
|
977
|
+
"""
|
|
978
|
+
return {
|
|
979
|
+
"ollama_tweaks": backend == "ollama",
|
|
980
|
+
"stream": backend in ("ollama", "openai"),
|
|
981
|
+
"openai_repairs": backend == "openai",
|
|
982
|
+
"budget": (20, 40) if backend == "openai" else (10, 20) if backend == "ollama" else (5, 12),
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
|
|
882
986
|
def _detect_model_id(base_url, api_key=""):
|
|
883
987
|
"""Ask an OpenAI-compatible server which model it serves (first reported id).
|
|
884
988
|
|
|
@@ -3509,11 +3613,12 @@ def run_agent_chat(cfg):
|
|
|
3509
3613
|
raw_coding_base = _normalize_openai_base(cfg.get("codingBaseURL") or "")
|
|
3510
3614
|
raw_coding_model = (cfg.get("codingModelId") or "").strip()
|
|
3511
3615
|
# Task #108: explicit coding backend kind + API key for an OpenAI-compatible endpoint.
|
|
3512
|
-
# coding_kind: "ollama" | "openai" | ""
|
|
3513
|
-
#
|
|
3514
|
-
#
|
|
3616
|
+
# coding_kind: "ollama" | "openai" | "" — the USER'S SETTING, not the answer. What the
|
|
3617
|
+
# branches read is `coding_backend` below, resolved once from this plus the URL.
|
|
3618
|
+
# coding_api_key: the Bearer key for an OpenAI-compatible coder (empty otherwise). Used
|
|
3619
|
+
# for the code-model calls (and coding-model auto-detect), NOT the chat agent_api_key.
|
|
3515
3620
|
coding_kind = (cfg.get("codingKind") or "").strip().lower()
|
|
3516
|
-
if coding_kind not in ("ollama", "openai"):
|
|
3621
|
+
if coding_kind not in ("ollama", "openai", "local"):
|
|
3517
3622
|
coding_kind = ""
|
|
3518
3623
|
coding_api_key = (cfg.get("codingApiKey") or "").strip()
|
|
3519
3624
|
# Task #107: when on, emit each code-model response so the worker persists it to Mongo.
|
|
@@ -3568,20 +3673,22 @@ def run_agent_chat(cfg):
|
|
|
3568
3673
|
# local MLX keeps the original tight default). Overridable via cfg.maxSteps.
|
|
3569
3674
|
# The provide_final_answer override below is only the exhaustion fallback.
|
|
3570
3675
|
#
|
|
3571
|
-
#
|
|
3572
|
-
#
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3676
|
+
# THE ONE RESOLUTION (task #117 → #123 Part B). Everything that used to ask "is this
|
|
3677
|
+
# Ollama?" or "did the user pick openai?" separately — reasoning_effort, streaming, the
|
|
3678
|
+
# #113 reasoning repairs, and this step budget — now reads `coding_backend`.
|
|
3679
|
+
#
|
|
3680
|
+
# The worker (gonext-local-worker.mjs) resolves this first, because it must decide
|
|
3681
|
+
# whether to fetch the coding API key BEFORE python starts, and passes the answer down
|
|
3682
|
+
# as codingBackend. Trust it only when we ended up on the URL it judged: the fallbacks
|
|
3683
|
+
# above can swap coding_base_url for the chat model's, which is a different server and
|
|
3684
|
+
# may well be a different KIND of server.
|
|
3685
|
+
_backend_hint = (cfg.get("codingBackend") or "").strip().lower()
|
|
3686
|
+
if _backend_hint in ("ollama", "openai", "local") and coding_base_url == raw_coding_base:
|
|
3687
|
+
coding_backend, _backend_why = _backend_hint, "resolved by the worker"
|
|
3583
3688
|
else:
|
|
3584
|
-
|
|
3689
|
+
coding_backend, _backend_why = _resolve_coding_backend(coding_kind, coding_base_url)
|
|
3690
|
+
coding_flags = _coding_backend_flags(coding_backend)
|
|
3691
|
+
_default_budget, _ws_budget = coding_flags["budget"]
|
|
3585
3692
|
try:
|
|
3586
3693
|
max_steps = int(cfg.get("maxSteps") or _default_budget)
|
|
3587
3694
|
except (TypeError, ValueError):
|
|
@@ -6268,25 +6375,16 @@ def run_agent_chat(cfg):
|
|
|
6268
6375
|
# the live test — minutes per step on a slow GPU — so disable it explicitly.
|
|
6269
6376
|
# Sniffed per-server because MLX might reject the unknown param.
|
|
6270
6377
|
extra_model_kwargs = {}
|
|
6271
|
-
#
|
|
6272
|
-
#
|
|
6273
|
-
|
|
6274
|
-
if coding_kind == "ollama":
|
|
6275
|
-
coding_is_ollama = True
|
|
6276
|
-
elif coding_kind == "openai":
|
|
6277
|
-
coding_is_ollama = False
|
|
6278
|
-
else:
|
|
6279
|
-
coding_is_ollama = _is_ollama_server(coding_base_url)
|
|
6280
|
-
if coding_is_ollama:
|
|
6378
|
+
# `coding_backend` was resolved once, above, and `coding_flags` derives everything
|
|
6379
|
+
# from it — do NOT re-derive any of this from the kind or a URL sniff here.
|
|
6380
|
+
if coding_flags["ollama_tweaks"]:
|
|
6281
6381
|
_log("coding server is Ollama → reasoning_effort='none' (disable thinking) + streaming")
|
|
6282
6382
|
extra_model_kwargs["reasoning_effort"] = "none"
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
_log(f"coding backend: kind={coding_kind or 'auto'} ollama={coding_is_ollama} "
|
|
6289
|
-
f"stream={_stream_coder} key={'yes' if coding_api_key else 'no'}")
|
|
6383
|
+
_stream_coder = coding_flags["stream"]
|
|
6384
|
+
_log(f"coding backend: {coding_backend} ({_backend_why}; "
|
|
6385
|
+
f"kind={coding_kind or 'auto'}, {_host_of(coding_base_url)}) "
|
|
6386
|
+
f"stream={_stream_coder} steps={max_steps} "
|
|
6387
|
+
f"key={'yes' if coding_api_key else 'no'}")
|
|
6290
6388
|
model = _LoggingModel(
|
|
6291
6389
|
model_id=coding_model_id,
|
|
6292
6390
|
api_base=coding_base_url,
|
|
@@ -6299,9 +6397,12 @@ def run_agent_chat(cfg):
|
|
|
6299
6397
|
# Task #107: persist each code-model response to Mongo when the user opted in.
|
|
6300
6398
|
save_full_response=save_full_response,
|
|
6301
6399
|
# Task #113: enables the reasoning-model repairs (prose-only nudge, parse-error
|
|
6302
|
-
# sanitizing, no-code breaker).
|
|
6303
|
-
# never take those branches.
|
|
6304
|
-
|
|
6400
|
+
# sanitizing, no-code breaker). Gated to the OpenAI-compatible backend — Ollama
|
|
6401
|
+
# and local MLX never take those branches. #113 gated this on the EXPLICIT
|
|
6402
|
+
# kind; the intent (don't touch Ollama) is unchanged, but an Auto+cloud config
|
|
6403
|
+
# now resolves to "openai" instead of "local", so it finally gets the repairs
|
|
6404
|
+
# it was always meant to have.
|
|
6405
|
+
openai_backend=coding_flags["openai_repairs"],
|
|
6305
6406
|
# Cap each HTTP attempt at 600s so a genuinely slow single call (a
|
|
6306
6407
|
# remote M6000/Vulkan box doing a cold ~4-5min prompt-eval) can finish
|
|
6307
6408
|
# on the first try, while a truly hung request still fails into OUR
|
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.333",
|
|
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",
|