prism-mcp-server 20.21.26 → 20.22.0
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/README.md +120 -0
- package/dist/tools/prismInferHandler.js +366 -78
- package/dist/utils/checkpointIdentity.js +230 -0
- package/dist/utils/layer1.js +113 -8
- package/dist/utils/omlxControl.js +73 -0
- package/dist/utils/omlxJobControl.js +373 -0
- package/dist/utils/omlxLayer1.js +54 -0
- package/dist/utils/omlxOwned.js +164 -0
- package/dist/utils/omlxProvider.js +791 -0
- package/dist/utils/safetyGate.js +7 -1
- package/dist/utils/workloadRouting.js +208 -0
- package/package.json +1 -1
- package/scripts/dev/browse.py +70 -19
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* directly — all cloud traffic goes via the synalux portal so billing,
|
|
20
20
|
* tier gating, and HIPAA audit are enforced in one place.
|
|
21
21
|
*/
|
|
22
|
+
import { ownedOmlxGenerate, recoverOwnedOmlx } from "../utils/omlxOwned.js";
|
|
23
|
+
import { OmlxError, createOmlxRuntime, guardedOmlxGenerate, omlxModelTarget, withInferenceResourceLease, withOllamaResourceLease, OMLX_MIN_AVAILABLE_BYTES } from "../utils/omlxProvider.js";
|
|
24
|
+
import { OMLX_LAYER1_MODEL, omlxLayer1Fetch } from "../utils/omlxLayer1.js";
|
|
22
25
|
import { createHash } from "node:crypto";
|
|
23
26
|
import { pickLocalModel, fmtGb, MODEL_TIERS, resolveOllamaName } from "../utils/modelPicker.js";
|
|
24
27
|
import { getSynaluxJwt, invalidateSynaluxJwt } from "../utils/synaluxJwt.js";
|
|
@@ -34,7 +37,7 @@ import { passesQualityGate } from "../utils/qualityGate.js";
|
|
|
34
37
|
import { passesClinicalQualityGate, clinicalPlanScaffold, formatClinicalSections, } from "../utils/clinicalQualityPolicy.js";
|
|
35
38
|
import { applyDeterministicCodingRepairs, buildCodingRepairPrompt, passesCodingQualityGate, } from "../utils/codingQualityPolicy.js";
|
|
36
39
|
import { checkInputSafety, checkOutputSafety } from "../utils/safetyGate.js";
|
|
37
|
-
import { callLayer1 as defaultCallLayer1, classifyDeterministicLayer1, keywordBackstop, reservedCategory, MAX_CLASSIFIER_PROMPT_LENGTH, layer1ClassifierContent, secondReadExclusion } from "../utils/layer1.js";
|
|
40
|
+
import { callLayer1 as defaultCallLayer1, classifyDeterministicLayer1, keywordBackstop, windowsOf, reservedCategory, MAX_CLASSIFIER_PROMPT_LENGTH, layer1ClassifierContent, secondReadExclusion } from "../utils/layer1.js";
|
|
38
41
|
import { getSecondReadPolicy, getAnswerCheckPolicy, getClassifierInputPolicy } from "../utils/inferencePolicy.js";
|
|
39
42
|
import { pseudonymizeForCheck } from "../utils/pseudonymize.js";
|
|
40
43
|
import { answerGroundingBytes, answerGroundingContent, parseGroundingVerdict, arithmeticSlips, arithmeticCorrection, ANSWER_GROUNDING_OUTPUT_TOKENS, ANSWER_GROUNDING_THINK, ANSWER_GROUNDING_THINK_TOKENS, ANSWER_GROUNDING_TIMEOUT_MS, ANSWER_GROUNDING_RETRY_TIMEOUT_MS, ANSWER_GROUNDING_FOLLOW_UP_TOKENS } from "../utils/answerGrounding.js";
|
|
@@ -182,28 +185,7 @@ export const HISTORY_TURN_WINDOW_OVERLAP = 200;
|
|
|
182
185
|
* one intent. */
|
|
183
186
|
export const DETERMINISTIC_FLOOR_WINDOW_CHARS = 7_200;
|
|
184
187
|
export const DETERMINISTIC_FLOOR_WINDOW_OVERLAP = 3_800;
|
|
185
|
-
export
|
|
186
|
-
if (content.length <= size)
|
|
187
|
-
return [content];
|
|
188
|
-
const isHigh = (i) => { const c = content.charCodeAt(i); return c >= 0xd800 && c <= 0xdbff; };
|
|
189
|
-
const isLow = (i) => { const c = content.charCodeAt(i); return c >= 0xdc00 && c <= 0xdfff; };
|
|
190
|
-
const out = [];
|
|
191
|
-
const step = size - overlap;
|
|
192
|
-
for (let i = 0; i < content.length; i += step) {
|
|
193
|
-
// Never cut a surrogate pair: a window that starts on a low or ends
|
|
194
|
-
// on a high surrogate is malformed text for the classifier.
|
|
195
|
-
let start = i;
|
|
196
|
-
if (start > 0 && isLow(start))
|
|
197
|
-
start += 1;
|
|
198
|
-
let end = Math.min(content.length, start + size);
|
|
199
|
-
if (end < content.length && isHigh(end - 1))
|
|
200
|
-
end += 1;
|
|
201
|
-
out.push(content.slice(start, end));
|
|
202
|
-
if (end >= content.length)
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
return out;
|
|
206
|
-
}
|
|
188
|
+
export { windowsOf };
|
|
207
189
|
export function historyTurnWindows(content) {
|
|
208
190
|
return windowsOf(content, HISTORY_TURN_WINDOW_CHARS, HISTORY_TURN_WINDOW_OVERLAP);
|
|
209
191
|
}
|
|
@@ -616,7 +598,8 @@ export const PRISM_INFER_TOOL = {
|
|
|
616
598
|
idempotentHint: false,
|
|
617
599
|
openWorldHint: true,
|
|
618
600
|
},
|
|
619
|
-
description: "Run
|
|
601
|
+
description: "Run local inference (Ollama by default). Explicit local_backend=omlx uses authenticated " +
|
|
602
|
+
"loopback oMLX for text-only code/reviewer drafts, without Ollama/cloud substitution or final sign-off. " +
|
|
620
603
|
"Owns model selection across 27B / 9B / 4B / 2B using an explicit `model_ceiling` or " +
|
|
621
604
|
"the caller's `task_complexity`, then validates loaded memory size, model context, " +
|
|
622
605
|
"entitlements, installed models, and free RAM at call time. " +
|
|
@@ -651,12 +634,11 @@ export const PRISM_INFER_TOOL = {
|
|
|
651
634
|
},
|
|
652
635
|
messages: {
|
|
653
636
|
type: "array",
|
|
654
|
-
description: "Prior turns of THIS conversation, oldest first;
|
|
655
|
-
"
|
|
656
|
-
"
|
|
657
|
-
"
|
|
658
|
-
"
|
|
659
|
-
"tier's context, forwarded to the cloud on escalation (32 KB cap), never stored.",
|
|
637
|
+
description: "Prior accepted text turns of THIS conversation, oldest first; prompt is current. " +
|
|
638
|
+
"Brief, not transcript. A paid Synalux plan feature; plan sets turn/character caps. " +
|
|
639
|
+
"Unavailable: multi_turn_not_in_plan. Malformed/over-cap: refused with caps named, " +
|
|
640
|
+
"never trimmed. Safety-screened, charged to context, forwarded on cloud escalation " +
|
|
641
|
+
"(32 KB cap), never stored.",
|
|
660
642
|
items: {
|
|
661
643
|
type: "object",
|
|
662
644
|
properties: {
|
|
@@ -680,6 +662,14 @@ export const PRISM_INFER_TOOL = {
|
|
|
680
662
|
description: "Sampling temperature; default 0 = deterministic.",
|
|
681
663
|
default: 0,
|
|
682
664
|
},
|
|
665
|
+
local_backend: {
|
|
666
|
+
type: "string", enum: ["ollama", "omlx"],
|
|
667
|
+
description: "Default Ollama; explicit oMLX is text-only code mode, no backend substitution.",
|
|
668
|
+
},
|
|
669
|
+
local_model: {
|
|
670
|
+
type: "string",
|
|
671
|
+
description: "Explicit oMLX only: exact advertised model ID; otherwise use trusted local configuration.",
|
|
672
|
+
},
|
|
683
673
|
model_ceiling: {
|
|
684
674
|
type: "string",
|
|
685
675
|
enum: ["27b", "9b", "4b", "2b"],
|
|
@@ -694,8 +684,7 @@ export const PRISM_INFER_TOOL = {
|
|
|
694
684
|
},
|
|
695
685
|
project: {
|
|
696
686
|
type: "string",
|
|
697
|
-
description: "
|
|
698
|
-
"local worker as historical data.",
|
|
687
|
+
description: "Project whose dashboard-depth handoff/recent session memory is historical worker context.",
|
|
699
688
|
},
|
|
700
689
|
context_depth: {
|
|
701
690
|
type: "string",
|
|
@@ -712,13 +701,12 @@ export const PRISM_INFER_TOOL = {
|
|
|
712
701
|
},
|
|
713
702
|
timeout_ms: {
|
|
714
703
|
type: "number",
|
|
715
|
-
description: "
|
|
704
|
+
description: "Timeout override. Ollama: 27B 120s, 9B 60s, 4B 20s, 2B 15s. oMLX generation/check budget: 300s.",
|
|
716
705
|
},
|
|
717
706
|
evidence: {
|
|
718
707
|
type: "array",
|
|
719
|
-
description: "
|
|
720
|
-
"
|
|
721
|
-
"or the draft is refused.",
|
|
708
|
+
description: "Evidence snippets. With verify:true every assertive claim must be ENTAILED by " +
|
|
709
|
+
"a snippet or the draft is refused.",
|
|
722
710
|
items: {
|
|
723
711
|
type: "object",
|
|
724
712
|
properties: {
|
|
@@ -730,9 +718,8 @@ export const PRISM_INFER_TOOL = {
|
|
|
730
718
|
},
|
|
731
719
|
verify: {
|
|
732
720
|
type: "boolean",
|
|
733
|
-
description: "L3 grounding
|
|
734
|
-
"
|
|
735
|
-
"CONTRADICTED claims are refused.",
|
|
721
|
+
description: "L3 grounding; default true with evidence. A second model (default qwen3.5:4b) " +
|
|
722
|
+
"checks the draft; NEUTRAL/CONTRADICTED claims are refused.",
|
|
736
723
|
},
|
|
737
724
|
verifier_model: {
|
|
738
725
|
type: "string",
|
|
@@ -755,8 +742,7 @@ export const PRISM_INFER_TOOL = {
|
|
|
755
742
|
type: "array",
|
|
756
743
|
maxItems: MAX_ROUTE_TOOLS,
|
|
757
744
|
items: { type: "string" },
|
|
758
|
-
description: "
|
|
759
|
-
"are suppressed in route mode. Default: Prism's seven trained routing tools.",
|
|
745
|
+
description: "Advertised route tools; other calls are suppressed. Default: Prism's seven trained tools.",
|
|
760
746
|
},
|
|
761
747
|
route_guard: {
|
|
762
748
|
type: "string",
|
|
@@ -772,8 +758,7 @@ export const PRISM_INFER_TOOL = {
|
|
|
772
758
|
},
|
|
773
759
|
think: {
|
|
774
760
|
type: "boolean",
|
|
775
|
-
description: "
|
|
776
|
-
"tasks, adds ~2-5s.",
|
|
761
|
+
description: "Reasoning override. Ollama uses tier/mode defaults; oMLX preserves local configuration when omitted.",
|
|
777
762
|
},
|
|
778
763
|
strict_entitlements: {
|
|
779
764
|
type: "boolean",
|
|
@@ -793,6 +778,32 @@ export const PRISM_INFER_TOOL = {
|
|
|
793
778
|
},
|
|
794
779
|
required: ["prompt"],
|
|
795
780
|
},
|
|
781
|
+
outputSchema: {
|
|
782
|
+
type: "object",
|
|
783
|
+
properties: {
|
|
784
|
+
output: { type: "string" },
|
|
785
|
+
backend: { type: "string" },
|
|
786
|
+
model_picked: { type: ["string", "null"] },
|
|
787
|
+
used_cloud: { type: "boolean" },
|
|
788
|
+
ram_free_mb: { type: "number" },
|
|
789
|
+
latency_ms: { type: "number" },
|
|
790
|
+
attempts: {
|
|
791
|
+
type: "array", items: {
|
|
792
|
+
type: "object", properties: { tier: { type: "string" }, reason: { type: "string" } },
|
|
793
|
+
required: ["tier", "reason"], additionalProperties: false,
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
gate_outcome: {
|
|
797
|
+
type: "object", properties: {
|
|
798
|
+
status: { type: "string", enum: ["success", "degraded", "refused"] },
|
|
799
|
+
reason: { type: "string" }, served_anyway: { type: "boolean" },
|
|
800
|
+
},
|
|
801
|
+
required: ["status", "served_anyway"], additionalProperties: false,
|
|
802
|
+
},
|
|
803
|
+
next_step: { type: "string" },
|
|
804
|
+
},
|
|
805
|
+
required: ["output", "backend", "model_picked", "used_cloud", "ram_free_mb", "latency_ms", "attempts"],
|
|
806
|
+
},
|
|
796
807
|
};
|
|
797
808
|
/** Why a `messages` value fails the structural (absolute) contract, or null
|
|
798
809
|
* when it passes. The MCP handler surfaces this text so an over-ceiling or
|
|
@@ -850,6 +861,12 @@ export function isPrismInferArgs(args) {
|
|
|
850
861
|
}
|
|
851
862
|
if (a.messages !== undefined && messagesProblem(a.messages) !== null)
|
|
852
863
|
return false;
|
|
864
|
+
if (a.local_backend !== undefined && a.local_backend !== "ollama" && a.local_backend !== "omlx")
|
|
865
|
+
return false;
|
|
866
|
+
if (a.local_model !== undefined && (a.local_backend !== "omlx" || typeof a.local_model !== "string" || !a.local_model.trim()))
|
|
867
|
+
return false;
|
|
868
|
+
if (a.local_backend === "omlx" && (a.mode !== "code" || (Array.isArray(a.images) && a.images.length > 0)))
|
|
869
|
+
return false;
|
|
853
870
|
if (a.max_tokens !== undefined && typeof a.max_tokens !== "number")
|
|
854
871
|
return false;
|
|
855
872
|
if (a.temperature !== undefined && typeof a.temperature !== "number")
|
|
@@ -1027,6 +1044,75 @@ export async function listOllamaLoaded(url = PRISM_LOCAL_LLM_URL) {
|
|
|
1027
1044
|
return new Set();
|
|
1028
1045
|
}
|
|
1029
1046
|
}
|
|
1047
|
+
/** The existing reversible Ollama unload operation, never a cache deletion. */
|
|
1048
|
+
export async function unloadOllamaModel(url, model, fetchImpl = fetch) {
|
|
1049
|
+
try {
|
|
1050
|
+
const response = await fetchImpl(`${url}/api/generate`, {
|
|
1051
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
1052
|
+
body: JSON.stringify({ model, keep_alive: 0 }),
|
|
1053
|
+
signal: AbortSignal.timeout(3_000), redirect: "error",
|
|
1054
|
+
});
|
|
1055
|
+
if (!response.ok)
|
|
1056
|
+
throw new Error();
|
|
1057
|
+
const acknowledgement = await response.json();
|
|
1058
|
+
if (acknowledgement.done !== true || acknowledgement.error !== undefined
|
|
1059
|
+
|| (acknowledgement.done_reason !== undefined && acknowledgement.done_reason !== "unload"))
|
|
1060
|
+
throw new Error();
|
|
1061
|
+
}
|
|
1062
|
+
catch {
|
|
1063
|
+
throw new OmlxError("omlx_ollama_unload_failed");
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
// Measured 2026-10-15: the 4b's unload is acknowledged in 38 ms and leaves
|
|
1067
|
+
// /api/ps about 250 ms later. The timeout leaves room for the larger tiers.
|
|
1068
|
+
const OLLAMA_UNLOAD_DRAIN_TIMEOUT_MS = 10_000;
|
|
1069
|
+
const OLLAMA_UNLOAD_POLL_INTERVAL_MS = 100;
|
|
1070
|
+
/** An unload acknowledgment can precede buffer release. Only a fresh empty
|
|
1071
|
+
* loaded set unlocks oMLX; never unload a newcomer while waiting. */
|
|
1072
|
+
export async function waitForOllamaUnloadDrain(listLoaded, initial, timeoutMs = OLLAMA_UNLOAD_DRAIN_TIMEOUT_MS, pollIntervalMs = OLLAMA_UNLOAD_POLL_INTERVAL_MS) {
|
|
1073
|
+
const deadline = Date.now() + timeoutMs;
|
|
1074
|
+
while (Date.now() < deadline) {
|
|
1075
|
+
let timer;
|
|
1076
|
+
let loaded;
|
|
1077
|
+
try {
|
|
1078
|
+
loaded = await Promise.race([
|
|
1079
|
+
Promise.resolve().then(listLoaded),
|
|
1080
|
+
new Promise((_resolve, reject) => {
|
|
1081
|
+
timer = setTimeout(() => reject(new OmlxError("omlx_ollama_state_unavailable")), deadline - Date.now());
|
|
1082
|
+
}),
|
|
1083
|
+
]);
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
throw new OmlxError("omlx_ollama_state_unavailable");
|
|
1087
|
+
}
|
|
1088
|
+
finally {
|
|
1089
|
+
if (timer)
|
|
1090
|
+
clearTimeout(timer);
|
|
1091
|
+
}
|
|
1092
|
+
if (Date.now() > deadline)
|
|
1093
|
+
break;
|
|
1094
|
+
if (loaded.size === 0)
|
|
1095
|
+
return;
|
|
1096
|
+
if ([...loaded].some(model => !initial.has(model)))
|
|
1097
|
+
throw new OmlxError("omlx_memory_contention");
|
|
1098
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(pollIntervalMs, Math.max(0, deadline - Date.now()))));
|
|
1099
|
+
}
|
|
1100
|
+
throw new OmlxError("omlx_ollama_unload_incomplete");
|
|
1101
|
+
}
|
|
1102
|
+
async function strictLoaded(url) {
|
|
1103
|
+
try {
|
|
1104
|
+
const response = await fetch(`${url}/api/ps`, { signal: AbortSignal.timeout(3_000), redirect: "error" });
|
|
1105
|
+
if (!response.ok)
|
|
1106
|
+
throw new Error();
|
|
1107
|
+
const data = await response.json();
|
|
1108
|
+
if (!Array.isArray(data.models) || data.models.some(model => typeof model?.name !== "string" || !model.name))
|
|
1109
|
+
throw new Error();
|
|
1110
|
+
return new Set(data.models.map(model => model.name));
|
|
1111
|
+
}
|
|
1112
|
+
catch {
|
|
1113
|
+
throw new OmlxError("omlx_ollama_state_unavailable");
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1030
1116
|
/** Resolve caller-supplied images to raw base64.
|
|
1031
1117
|
* A filesystem path is read and encoded; anything else is assumed to be
|
|
1032
1118
|
* base64 already. A path that cannot be read THROWS rather than being passed
|
|
@@ -1841,6 +1927,35 @@ const _evictionMutex = (() => {
|
|
|
1841
1927
|
};
|
|
1842
1928
|
})();
|
|
1843
1929
|
export async function runInfer(args, deps) {
|
|
1930
|
+
const started = Date.now();
|
|
1931
|
+
return withInferenceResourceLease(args.local_backend === "omlx", async () => {
|
|
1932
|
+
let releaseDevice;
|
|
1933
|
+
const runtime = args.local_backend === "omlx" ? deps.omlx : undefined;
|
|
1934
|
+
// Every owned generation, so the device outlives its marker writes.
|
|
1935
|
+
const generations = new Set();
|
|
1936
|
+
const scopedDeps = runtime ? { ...deps, omlx: { ...runtime, detect: async (model) => {
|
|
1937
|
+
const target = await runtime.detect(model);
|
|
1938
|
+
releaseDevice = await runtime.acquire(undefined, { recoveryProviderId: target.url });
|
|
1939
|
+
// Before Layer 1 or anything else: a stale marker of this provider
|
|
1940
|
+
// is cleared on proof, or the call is refused.
|
|
1941
|
+
await runtime.recover?.(target);
|
|
1942
|
+
return target;
|
|
1943
|
+
}, generate: (target, request, signal) => {
|
|
1944
|
+
const generation = runtime.generate(target, request, signal);
|
|
1945
|
+
generations.add(generation.catch(() => undefined));
|
|
1946
|
+
return generation;
|
|
1947
|
+
} } } : deps;
|
|
1948
|
+
try {
|
|
1949
|
+
return { ...await runInferWithResources(args, scopedDeps), latency_ms: Date.now() - started };
|
|
1950
|
+
}
|
|
1951
|
+
finally {
|
|
1952
|
+
// guardedOmlxGenerate may answer first; the lease waits for the jobs.
|
|
1953
|
+
await Promise.all(generations);
|
|
1954
|
+
await releaseDevice?.();
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
async function runInferWithResources(args, deps) {
|
|
1844
1959
|
const t0 = Date.now();
|
|
1845
1960
|
const temperature = args.temperature ?? 0;
|
|
1846
1961
|
// ── L1 Safety — deterministic input interception ────────────
|
|
@@ -1867,6 +1982,11 @@ export async function runInfer(args, deps) {
|
|
|
1867
1982
|
history_turns: args.messages?.length ?? 0,
|
|
1868
1983
|
};
|
|
1869
1984
|
}
|
|
1985
|
+
return args.local_backend === "omlx"
|
|
1986
|
+
? runInferAfterInputSafety(args, deps, t0, temperature)
|
|
1987
|
+
: withOllamaResourceLease(() => runInferAfterInputSafety(args, deps, t0, temperature));
|
|
1988
|
+
}
|
|
1989
|
+
async function runInferAfterInputSafety(args, deps, t0, temperature) {
|
|
1870
1990
|
// ── Entitlement enforcement ──────────────────────────────────
|
|
1871
1991
|
// Resolved per call (§5.5) — getEntitlements dedupes via a 5-min cache.
|
|
1872
1992
|
// Free users without auth get 4b ceiling, 50 calls/day, 512 max tokens.
|
|
@@ -1923,7 +2043,7 @@ export async function runInfer(args, deps) {
|
|
|
1923
2043
|
// explicit request still behaves as before and is refused at the point of
|
|
1924
2044
|
// use with its attempt named.
|
|
1925
2045
|
const planDefault = ent.features.cloud_fallback && !(args.images?.length);
|
|
1926
|
-
let allowCloud = (args.cloud_fallback ?? planDefault) && ent.features.cloud_fallback;
|
|
2046
|
+
let allowCloud = args.local_backend !== "omlx" && (args.cloud_fallback ?? planDefault) && ent.features.cloud_fallback;
|
|
1927
2047
|
// Verification only for paid plans (free users skip L3 grounding)
|
|
1928
2048
|
const canVerify = ent.features.grounding_verifier;
|
|
1929
2049
|
// The portal entitlement is authoritative. A paid plan alone must not
|
|
@@ -2076,8 +2196,78 @@ export async function runInfer(args, deps) {
|
|
|
2076
2196
|
// Discover which tags Ollama actually has + which are already warm.
|
|
2077
2197
|
// Already-loaded models don't need RAM headroom — they're reusing
|
|
2078
2198
|
// memory Ollama allocated previously.
|
|
2199
|
+
let omlxTarget;
|
|
2200
|
+
const omlxRuntime = args.local_backend === "omlx" ? deps.omlx : undefined;
|
|
2201
|
+
if (args.local_backend === "omlx") {
|
|
2202
|
+
if (mode !== "code" || args.images?.length)
|
|
2203
|
+
throw new Error("prism_infer: oMLX requires text-only code mode");
|
|
2204
|
+
if (!omlxRuntime)
|
|
2205
|
+
throw new Error("prism_infer: omlx_runtime_unavailable");
|
|
2206
|
+
// No trusted binding maps an oMLX model to a logical tier, and the
|
|
2207
|
+
// configured Flash model outweighs every Ollama tier. Below the top
|
|
2208
|
+
// ceiling, from the caller or the plan, refuse before any network,
|
|
2209
|
+
// classification or residency change.
|
|
2210
|
+
if (effectiveCeiling !== "27b") {
|
|
2211
|
+
attempts.push({ tier: "omlx", reason: "omlx_model_ceiling" });
|
|
2212
|
+
if (wantReport)
|
|
2213
|
+
return refusedResult("omlx_model_ceiling");
|
|
2214
|
+
throw new Error("prism_infer: omlx_model_ceiling; explicit oMLX needs the top model ceiling");
|
|
2215
|
+
}
|
|
2216
|
+
if (!Number.isInteger(localMaxTokens) || localMaxTokens <= 0
|
|
2217
|
+
|| !Number.isFinite(temperature) || temperature < 0
|
|
2218
|
+
|| (args.timeout_ms !== undefined && (!Number.isInteger(args.timeout_ms) || args.timeout_ms <= 0 || args.timeout_ms > 900_000))) {
|
|
2219
|
+
throw new Error("prism_infer: invalid bounded oMLX generation parameters");
|
|
2220
|
+
}
|
|
2221
|
+
try {
|
|
2222
|
+
omlxTarget = await omlxRuntime.detect(args.local_model);
|
|
2223
|
+
}
|
|
2224
|
+
catch (error) {
|
|
2225
|
+
const reason = error instanceof OmlxError ? error.reason : "omlx_detection_unavailable";
|
|
2226
|
+
attempts.push({ tier: "omlx", reason });
|
|
2227
|
+
if (wantReport)
|
|
2228
|
+
return refusedResult(reason);
|
|
2229
|
+
throw new Error(`prism_infer: ${reason}; explicit oMLX will not substitute another backend`);
|
|
2230
|
+
}
|
|
2231
|
+
// Fit is conservative in UTF-8 bytes; refuse before changing Ollama state.
|
|
2232
|
+
const inputBytes = Buffer.byteLength(args.prompt + (args.system ?? ""), "utf8")
|
|
2233
|
+
+ (args.messages ?? []).reduce((n, turn) => n + Buffer.byteLength(turn.content, "utf8") + 8, 0);
|
|
2234
|
+
if (inputBytes + localMaxTokens + 1_300 > omlxTarget.contextTokens) {
|
|
2235
|
+
throw new Error("prism_infer: omlx_ctx_insufficient");
|
|
2236
|
+
}
|
|
2237
|
+
attempts.push({ tier: "omlx", reason: "authenticated_model_detected" });
|
|
2238
|
+
}
|
|
2079
2239
|
const installed = await deps.listTags();
|
|
2080
2240
|
const loaded = await deps.listLoaded();
|
|
2241
|
+
if (omlxTarget && installed === null)
|
|
2242
|
+
throw new Error("prism_infer: omlx_classifier_unavailable");
|
|
2243
|
+
// Ollama is unloaded during an explicit oMLX call (owner, 2026-10-15; this
|
|
2244
|
+
// replaces plan section 4's "never evict another Ollama user's weights").
|
|
2245
|
+
// Under the device lease, before classification: unload every resident
|
|
2246
|
+
// model reversibly (the weights stay on disk and reload on the next Ollama
|
|
2247
|
+
// call) and wait until Ollama reports none. A failed unload, a model that
|
|
2248
|
+
// does not drain, or a newcomer loading meanwhile refuses. After this, a
|
|
2249
|
+
// model that appears is refused per read by guardedOmlxGenerate.
|
|
2250
|
+
if (omlxTarget && omlxRuntime) {
|
|
2251
|
+
let residency = null;
|
|
2252
|
+
try {
|
|
2253
|
+
const initial = new Set(await omlxRuntime.listLoaded());
|
|
2254
|
+
for (const model of initial)
|
|
2255
|
+
await omlxRuntime.unload(model);
|
|
2256
|
+
if (initial.size > 0) {
|
|
2257
|
+
await waitForOllamaUnloadDrain(() => omlxRuntime.listLoaded(), initial);
|
|
2258
|
+
attempts.push({ tier: "omlx", reason: `omlx_ollama_unloaded:${initial.size}` });
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
catch (error) {
|
|
2262
|
+
residency = error instanceof OmlxError ? error.reason : "omlx_ollama_state_unavailable";
|
|
2263
|
+
}
|
|
2264
|
+
if (residency) {
|
|
2265
|
+
attempts.push({ tier: "omlx", reason: residency });
|
|
2266
|
+
if (wantReport)
|
|
2267
|
+
return refusedResult(residency);
|
|
2268
|
+
throw new Error(`prism_infer: ${residency}; oMLX generation refused`);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2081
2271
|
if (installed === null) {
|
|
2082
2272
|
attempts.push({ tier: "ollama_probe", reason: "unreachable" });
|
|
2083
2273
|
}
|
|
@@ -2129,8 +2319,21 @@ export async function runInfer(args, deps) {
|
|
|
2129
2319
|
}
|
|
2130
2320
|
let secondReadModel = null;
|
|
2131
2321
|
if (installed) {
|
|
2132
|
-
|
|
2133
|
-
|
|
2322
|
+
// Explicit oMLX classifies on its own server: every read (the prompt,
|
|
2323
|
+
// history and context windows) goes to the oMLX classifier through the
|
|
2324
|
+
// owned path, inside this call's device lease. Nothing is loaded into
|
|
2325
|
+
// Ollama, and the history cache keys carry the oMLX model's name.
|
|
2326
|
+
// A target that cannot carry the credential fails the read (ERROR), and
|
|
2327
|
+
// explicit oMLX refuses ERROR; it never falls back to Ollama.
|
|
2328
|
+
const baseL1 = deps.callLayer1 ?? defaultCallLayer1;
|
|
2329
|
+
const generationTarget = omlxTarget, runtime = omlxRuntime;
|
|
2330
|
+
const omlxL1Fetch = generationTarget && runtime
|
|
2331
|
+
? omlxLayer1Fetch(async (request) => guardedOmlxGenerate(runtime, omlxModelTarget(generationTarget, OMLX_LAYER1_MODEL), request, deps.freemem))
|
|
2332
|
+
: undefined;
|
|
2333
|
+
const l1fn = omlxL1Fetch
|
|
2334
|
+
? (...[prompt, url, model, , images, opts]) => baseL1(prompt, url, model, omlxL1Fetch, images, opts)
|
|
2335
|
+
: baseL1;
|
|
2336
|
+
const l1Model = omlxL1Fetch ? OMLX_LAYER1_MODEL : resolveOllamaName("prism-coder:4b", installed);
|
|
2134
2337
|
secondReadModel = null;
|
|
2135
2338
|
// The classifier must be able to SEE what it is classifying. Ollama
|
|
2136
2339
|
// accepts `images` on a text-only model and silently ignores them
|
|
@@ -2156,6 +2359,9 @@ export async function runInfer(args, deps) {
|
|
|
2156
2359
|
`Rebuild the classifier with a vision tower to enable image requests.`);
|
|
2157
2360
|
}
|
|
2158
2361
|
}
|
|
2362
|
+
// Explicit oMLX: errors are never served (strictErrors), and the rules
|
|
2363
|
+
// only refuse; the classifier reads everything they would clear.
|
|
2364
|
+
const omlxL1Opts = omlxTarget ? { strictErrors: true, readOversizeWhole: true } : undefined;
|
|
2159
2365
|
// 4th arg is fetchImpl (default), 5th is the images the classifier must see.
|
|
2160
2366
|
// Single turn: one call, unchanged. With history, three layers: the
|
|
2161
2367
|
// deterministic floor per turn, every turn read alone (every verdict
|
|
@@ -2169,8 +2375,8 @@ export async function runInfer(args, deps) {
|
|
|
2169
2375
|
if (!args.messages?.length) {
|
|
2170
2376
|
// Single turn: one call; the classifier-input policy is passed when there is one.
|
|
2171
2377
|
l1 = classifierInput
|
|
2172
|
-
? await l1fn(args.prompt, deps.ollamaUrl, l1Model, undefined, resolvedImages, { classifierInput })
|
|
2173
|
-
: await l1fn(args.prompt, deps.ollamaUrl, l1Model, undefined, resolvedImages);
|
|
2378
|
+
? await l1fn(args.prompt, deps.ollamaUrl, l1Model, undefined, resolvedImages, { classifierInput, ...omlxL1Opts })
|
|
2379
|
+
: await l1fn(args.prompt, deps.ollamaUrl, l1Model, undefined, resolvedImages, ...(omlxL1Opts ? [omlxL1Opts] : []));
|
|
2174
2380
|
if (l1 !== "OBVIOUS_NOT_RESERVED")
|
|
2175
2381
|
l1Layer = "prompt";
|
|
2176
2382
|
}
|
|
@@ -2252,7 +2458,7 @@ export async function runInfer(args, deps) {
|
|
|
2252
2458
|
// is switched off here — it would undo the slicing above (words
|
|
2253
2459
|
// 14k chars apart firing one rule; review round 18).
|
|
2254
2460
|
if (l1 !== "OBVIOUS_RESERVED") {
|
|
2255
|
-
l1 = raise(l1, await l1fn(args.prompt, deps.ollamaUrl, l1Model, undefined, resolvedImages, { deterministic: false, ...(classifierInput ? { classifierInput } : {}) }), "prompt");
|
|
2461
|
+
l1 = raise(l1, await l1fn(args.prompt, deps.ollamaUrl, l1Model, undefined, resolvedImages, { deterministic: false, ...(classifierInput ? { classifierInput } : {}), ...omlxL1Opts }), "prompt");
|
|
2256
2462
|
}
|
|
2257
2463
|
// 3. Context, raise only: one window per turn and one for the
|
|
2258
2464
|
// prompt (see contextWindows), cached like any window. Skipped
|
|
@@ -2314,6 +2520,12 @@ export async function runInfer(args, deps) {
|
|
|
2314
2520
|
attempts.push({ tier: "layer1", reason: "layer1_hedge_second_read_skipped_classifier_error" });
|
|
2315
2521
|
entMeta.layer1_second_read = "skipped_classifier_error";
|
|
2316
2522
|
}
|
|
2523
|
+
else if (modelHedgeOnly && omlxL1Fetch) {
|
|
2524
|
+
// On explicit oMLX the 9b already read every window: the hedge
|
|
2525
|
+
// stands and refuses.
|
|
2526
|
+
attempts.push({ tier: "layer1", reason: "layer1_hedge_second_read_skipped_omlx" });
|
|
2527
|
+
entMeta.layer1_second_read = "skipped_omlx";
|
|
2528
|
+
}
|
|
2317
2529
|
else if (modelHedgeOnly) {
|
|
2318
2530
|
const second = await layer1HedgeSecondRead({
|
|
2319
2531
|
l1fn, args, installed, listLoaded: deps.listLoaded, freeBytes: deps.freemem(), ollamaUrl: deps.ollamaUrl,
|
|
@@ -2491,11 +2703,31 @@ export async function runInfer(args, deps) {
|
|
|
2491
2703
|
throw new Error(`prism_infer: classifier failed + keyword backstop caught reserved content. attempts=${JSON.stringify(attempts)}`);
|
|
2492
2704
|
}
|
|
2493
2705
|
}
|
|
2706
|
+
// Explicit oMLX never serves on the keyword backstop alone: a classifier
|
|
2707
|
+
// that could not answer a read is not permission (plan section 4), and
|
|
2708
|
+
// there is no cloud to escalate to. Ordinary callers keep their policy.
|
|
2709
|
+
if (omlxTarget && l1 === "ERROR") {
|
|
2710
|
+
attempts.push({ tier: "omlx", reason: "omlx_classifier_error" });
|
|
2711
|
+
if (wantReport)
|
|
2712
|
+
return refusedResult("omlx_classifier_error");
|
|
2713
|
+
throw new Error("prism_infer: omlx_classifier_error; explicit oMLX does not serve on the keyword backstop alone");
|
|
2714
|
+
}
|
|
2494
2715
|
if (l1 === "OBVIOUS_NOT_RESERVED") {
|
|
2495
2716
|
debugLog(`[prism_infer] Layer 1 verdict=OBVIOUS_NOT_RESERVED — proceeding local`);
|
|
2496
2717
|
}
|
|
2497
2718
|
}
|
|
2498
2719
|
// ── end Layer 1 ─────────────────────────────────────────────────────────
|
|
2720
|
+
// Explicit oMLX classified on oMLX, so nothing of this call's is in Ollama.
|
|
2721
|
+
// Anything resident now belongs to another user: guardedOmlxGenerate
|
|
2722
|
+
// refuses generation (omlx_memory_contention) before any request.
|
|
2723
|
+
const omlxDeadline = Date.now() + (args.timeout_ms ?? 300_000);
|
|
2724
|
+
const callLocal = omlxTarget && omlxRuntime
|
|
2725
|
+
? async (_url, _model, prompt, system, maxTokens, temperature, timeoutMs, think, _images, history) => {
|
|
2726
|
+
const messages = [...(system === undefined ? [] : [{ role: "system", content: system }]),
|
|
2727
|
+
...(history ?? []), { role: "user", content: prompt }];
|
|
2728
|
+
return guardedOmlxGenerate(omlxRuntime, omlxTarget, { messages, maxTokens, temperature: args.temperature, timeoutMs: Math.min(timeoutMs, omlxDeadline - Date.now()), think }, deps.freemem);
|
|
2729
|
+
}
|
|
2730
|
+
: deps.callLocal;
|
|
2499
2731
|
// A second read ran the 9b after free RAM and the warm set were read
|
|
2500
2732
|
// above (and may have loaded it, or found it unloaded since); eviction and
|
|
2501
2733
|
// the tier RAM gate below must see the machine as it is now. Every other
|
|
@@ -2513,19 +2745,20 @@ export async function runInfer(args, deps) {
|
|
|
2513
2745
|
// A local answer to a conversation the answer check rejected or did not check: never served as answer text.
|
|
2514
2746
|
let withheldReason = null;
|
|
2515
2747
|
if (installed) {
|
|
2748
|
+
const servingTiers = omlxTarget ? [{ tag: omlxTarget.model, weightsGb: 0, minFreeGb: OMLX_MIN_AVAILABLE_BYTES / 1024 ** 3, ctxTokens: omlxTarget.contextTokens }] : MODEL_TIERS;
|
|
2516
2749
|
// F4 fix: guard ceiling-not-found — Math.max(0,-1) silently targets tier 0 (27b).
|
|
2517
2750
|
// Instead of defaulting to the largest tier, treat not-found as "no ceiling" (start=0).
|
|
2518
2751
|
const ceilIdx = effectiveCeiling
|
|
2519
2752
|
? MODEL_TIERS.findIndex(t => t.tag.endsWith(`:${effectiveCeiling}`))
|
|
2520
2753
|
: -1;
|
|
2521
|
-
const ceilStart = ceilIdx >= 0 ? ceilIdx : 0;
|
|
2754
|
+
const ceilStart = omlxTarget ? 0 : ceilIdx >= 0 ? ceilIdx : 0;
|
|
2522
2755
|
// Auto-evict: if the ceiling tier is installed but not warm and prism's
|
|
2523
2756
|
// own smaller tier models are warm, unload them to make room.
|
|
2524
2757
|
// Operates only on prism tier models — never evicts arbitrary Ollama models
|
|
2525
2758
|
// the caller doesn't own (F1). Uses an in-process mutex to prevent a
|
|
2526
2759
|
// concurrent request from evicting a model mid-inference (F3).
|
|
2527
|
-
let freeAfterEvict = genFreeBytes;
|
|
2528
|
-
if (genLoaded && genLoaded.size > 0) {
|
|
2760
|
+
let freeAfterEvict = omlxTarget ? deps.freemem() : genFreeBytes;
|
|
2761
|
+
if (!omlxTarget && genLoaded && genLoaded.size > 0) {
|
|
2529
2762
|
const ceilTier = MODEL_TIERS[ceilIdx >= 0 ? ceilIdx : 0];
|
|
2530
2763
|
const ceilName = ceilTier ? resolveOllamaName(ceilTier.tag, installed) : null;
|
|
2531
2764
|
const ceilInstalled = ceilName ? installed.has(ceilName) : false;
|
|
@@ -2579,11 +2812,7 @@ export async function runInfer(args, deps) {
|
|
|
2579
2812
|
const released = await _evictionMutex.acquire();
|
|
2580
2813
|
try {
|
|
2581
2814
|
// F2 fix: await each evict call; log failures; don't proceed blind.
|
|
2582
|
-
const evictResults = await Promise.allSettled(tierModelsToEvict.map(m =>
|
|
2583
|
-
method: "POST",
|
|
2584
|
-
body: JSON.stringify({ model: m, keep_alive: 0 }),
|
|
2585
|
-
signal: AbortSignal.timeout(3_000),
|
|
2586
|
-
})));
|
|
2815
|
+
const evictResults = await Promise.allSettled(tierModelsToEvict.map(m => unloadOllamaModel(deps.ollamaUrl, m)));
|
|
2587
2816
|
const failed = evictResults.filter(r => r.status === "rejected").length;
|
|
2588
2817
|
if (failed > 0) {
|
|
2589
2818
|
debugLog(`[prism_infer] evict: ${failed}/${tierModelsToEvict.length} unload requests failed`);
|
|
@@ -2669,14 +2898,14 @@ export async function runInfer(args, deps) {
|
|
|
2669
2898
|
// so the tier cannot be chosen on it, and a digit that is silently
|
|
2670
2899
|
// wrong reads exactly like a digit that is right. The saving was worth
|
|
2671
2900
|
// less than measured; the failure is worth more.
|
|
2672
|
-
for (let i = ceilStart; i <
|
|
2673
|
-
const tier =
|
|
2901
|
+
for (let i = ceilStart; i < servingTiers.length; i++) {
|
|
2902
|
+
const tier = servingTiers[i];
|
|
2674
2903
|
// Accept the tier whether Ollama reports it as bare (`prism-coder:27b`)
|
|
2675
2904
|
// or namespaced (`dcostenco/prism-coder:27b`, the form `ollama pull`
|
|
2676
2905
|
// produces from a HF repo). resolveOllamaName returns the actual
|
|
2677
2906
|
// name Ollama knows so /api/generate finds the model.
|
|
2678
|
-
const ollamaName = resolveOllamaName(tier.tag, installed);
|
|
2679
|
-
if (!installed.has(ollamaName)) {
|
|
2907
|
+
const ollamaName = omlxTarget ? omlxTarget.model : resolveOllamaName(tier.tag, installed);
|
|
2908
|
+
if (!omlxTarget && !installed.has(ollamaName)) {
|
|
2680
2909
|
attempts.push({ tier: tier.tag, reason: "not_pulled" });
|
|
2681
2910
|
continue;
|
|
2682
2911
|
}
|
|
@@ -2713,7 +2942,7 @@ export async function runInfer(args, deps) {
|
|
|
2713
2942
|
// unprobeable means fall back to the table, never unlock.
|
|
2714
2943
|
let liveCtx = null;
|
|
2715
2944
|
try {
|
|
2716
|
-
liveCtx = await (deps.probeNumCtx ?? probeNumCtx)(deps.ollamaUrl, ollamaName);
|
|
2945
|
+
liveCtx = omlxTarget ? omlxTarget.contextTokens : await (deps.probeNumCtx ?? probeNumCtx)(deps.ollamaUrl, ollamaName);
|
|
2717
2946
|
}
|
|
2718
2947
|
catch {
|
|
2719
2948
|
liveCtx = null;
|
|
@@ -2732,12 +2961,16 @@ export async function runInfer(args, deps) {
|
|
|
2732
2961
|
// shape) and cached, including negatives.
|
|
2733
2962
|
const MAX_TEMPLATE_OVERHEAD = 1_300;
|
|
2734
2963
|
let CTX_TEMPLATE_MARGIN = 64;
|
|
2735
|
-
if (promptBodyEst + MAX_TEMPLATE_OVERHEAD > effectiveCtx) {
|
|
2964
|
+
if (!omlxTarget && promptBodyEst + MAX_TEMPLATE_OVERHEAD > effectiveCtx) {
|
|
2736
2965
|
const probed = await (deps.probeTemplateOverhead ?? probeTemplateOverhead)(deps.ollamaUrl, ollamaName, effectiveSystem != null);
|
|
2737
2966
|
if (probed != null)
|
|
2738
2967
|
CTX_TEMPLATE_MARGIN = probed;
|
|
2739
2968
|
}
|
|
2740
|
-
const promptTokensEst =
|
|
2969
|
+
const promptTokensEst = omlxTarget
|
|
2970
|
+
? Buffer.byteLength(args.prompt + (effectiveSystem ?? ""), "utf8")
|
|
2971
|
+
+ (args.messages ?? []).reduce((n, turn) => n + Buffer.byteLength(turn.content, "utf8") + 8, 0)
|
|
2972
|
+
+ MAX_TEMPLATE_OVERHEAD + localMaxTokens
|
|
2973
|
+
: promptBodyEst + CTX_TEMPLATE_MARGIN;
|
|
2741
2974
|
if (promptTokensEst > effectiveCtx) {
|
|
2742
2975
|
attempts.push({
|
|
2743
2976
|
tier: tier.tag,
|
|
@@ -2752,22 +2985,22 @@ export async function runInfer(args, deps) {
|
|
|
2752
2985
|
continue;
|
|
2753
2986
|
}
|
|
2754
2987
|
anyViable = true;
|
|
2755
|
-
const timeout = args.timeout_ms ?? DEFAULT_TIMEOUTS[tier.tag] ?? 60_000;
|
|
2756
|
-
const enableThink = resolveThinkingMode(args, mode, tier);
|
|
2988
|
+
const timeout = args.timeout_ms ?? (omlxTarget ? 300_000 : DEFAULT_TIMEOUTS[tier.tag] ?? 60_000);
|
|
2989
|
+
const enableThink = omlxTarget ? args.think : resolveThinkingMode(args, mode, tier);
|
|
2757
2990
|
// Reasoning must not crowd out the answer: a tier that thinks needs
|
|
2758
2991
|
// its own floor, or the hard_truncation retry cuts the thinking back
|
|
2759
2992
|
// off and lands on the configuration this tier is worst in.
|
|
2760
2993
|
const tierTokens = enableThink && tier.minLocalTokens
|
|
2761
2994
|
? Math.min(Math.max(localMaxTokens, tier.minLocalTokens), 8192)
|
|
2762
2995
|
: localMaxTokens;
|
|
2763
|
-
let result = await
|
|
2996
|
+
let result = await callLocal(deps.ollamaUrl, ollamaName, args.prompt, effectiveSystem, tierTokens, temperature, timeout, enableThink, resolvedImages, ...historyArgs(args));
|
|
2764
2997
|
// Think-only retry: model burned all tokens on <think>, empty content.
|
|
2765
2998
|
// Retry same model with think=false rather than falling to a smaller tier.
|
|
2766
2999
|
// One-shot: think=false cannot re-trigger think_only (no thinking to burn).
|
|
2767
|
-
if (!result.ok && result.reason === "think_only" && enableThink) {
|
|
3000
|
+
if (!omlxTarget && !result.ok && result.reason === "think_only" && enableThink) {
|
|
2768
3001
|
debugLog(`[prism_infer] ${tier.tag} returned think-only — retrying with think=false`);
|
|
2769
3002
|
recordThinkOnlyRetry();
|
|
2770
|
-
result = await
|
|
3003
|
+
result = await callLocal(deps.ollamaUrl, ollamaName, args.prompt, effectiveSystem, tierTokens, temperature, timeout, false, resolvedImages, ...historyArgs(args));
|
|
2771
3004
|
}
|
|
2772
3005
|
if (result.ok) {
|
|
2773
3006
|
// Did ollama silently drop part of the prompt?
|
|
@@ -2834,7 +3067,7 @@ export async function runInfer(args, deps) {
|
|
|
2834
3067
|
// collapses (adversarial review 2026-09-16), and the prompt
|
|
2835
3068
|
// alone would never reach the floor.
|
|
2836
3069
|
const inputChars = args.prompt.length + historyChars(args);
|
|
2837
|
-
const looksTruncated = halfCtx != null
|
|
3070
|
+
const looksTruncated = !omlxTarget && halfCtx != null
|
|
2838
3071
|
&& result.promptTokens != null
|
|
2839
3072
|
&& Math.abs(result.promptTokens - halfCtx) <= 8
|
|
2840
3073
|
&& inputChars >= halfCtx;
|
|
@@ -2879,10 +3112,10 @@ export async function runInfer(args, deps) {
|
|
|
2879
3112
|
// there); this is chat/code on a small budget. One shot only —
|
|
2880
3113
|
// think=false has no reasoning left to cut, so a second retry would
|
|
2881
3114
|
// be pure latency.
|
|
2882
|
-
if (!gate.pass && gate.reason === "hard_truncation" && enableThink) {
|
|
3115
|
+
if (!omlxTarget && !gate.pass && gate.reason === "hard_truncation" && enableThink) {
|
|
2883
3116
|
debugLog(`[prism_infer] ${tier.tag} truncated mid-answer — retrying with think=false`);
|
|
2884
3117
|
attempts.push({ tier: tier.tag, reason: "hard_truncation_retry" });
|
|
2885
|
-
const retried = await
|
|
3118
|
+
const retried = await callLocal(deps.ollamaUrl, ollamaName, args.prompt, effectiveSystem, tierTokens, temperature, timeout, false, resolvedImages, ...historyArgs(args));
|
|
2886
3119
|
if (retried.ok) {
|
|
2887
3120
|
const retriedStrip = stripThink(retried.text);
|
|
2888
3121
|
const retriedGate = passesQualityGate(retriedStrip.stripped, retriedStrip.thinkOnly, retried.doneReason, mode, gateOptions);
|
|
@@ -2944,7 +3177,7 @@ export async function runInfer(args, deps) {
|
|
|
2944
3177
|
// Same images and history as the first call: a repair
|
|
2945
3178
|
// of a follow-up without its context "repairs" against
|
|
2946
3179
|
// nothing (adversarial review 2026-09-16).
|
|
2947
|
-
const repaired = await
|
|
3180
|
+
const repaired = await callLocal(deps.ollamaUrl, ollamaName, repair.prompt, repairSystem, maxTokens, 0, timeout, omlxTarget ? args.think : false, resolvedImages, ...historyArgs(args));
|
|
2948
3181
|
if (repaired.ok) {
|
|
2949
3182
|
const repairedStripped = stripThink(repaired.text);
|
|
2950
3183
|
const repairedGenericGate = passesQualityGate(repairedStripped.stripped, repairedStripped.thinkOnly, repaired.doneReason, mode);
|
|
@@ -3011,9 +3244,9 @@ export async function runInfer(args, deps) {
|
|
|
3011
3244
|
// overhead, the reasoning budget and the one follow-up turn: a
|
|
3012
3245
|
// request Ollama truncates would let the check pass an answer
|
|
3013
3246
|
// against a conversation it only partly read.
|
|
3014
|
-
const runtimeCtx = await (deps.probeLoadedContext ?? probeLoadedContext)(deps.ollamaUrl, ollamaName).catch(() => null);
|
|
3247
|
+
const runtimeCtx = omlxTarget ? omlxTarget.contextTokens : await (deps.probeLoadedContext ?? probeLoadedContext)(deps.ollamaUrl, ollamaName).catch(() => null);
|
|
3015
3248
|
const checkCtx = runtimeCtx ?? effectiveCtx;
|
|
3016
|
-
const overhead = (await Promise.resolve((deps.probeTemplateOverhead ?? probeTemplateOverhead)(deps.ollamaUrl, ollamaName, true)).catch(() => null)) ?? MAX_TEMPLATE_OVERHEAD;
|
|
3249
|
+
const overhead = omlxTarget ? MAX_TEMPLATE_OVERHEAD : (await Promise.resolve((deps.probeTemplateOverhead ?? probeTemplateOverhead)(deps.ollamaUrl, ollamaName, true)).catch(() => null)) ?? MAX_TEMPLATE_OVERHEAD;
|
|
3017
3250
|
const need = answerGroundingBytes(policy, args.messages ?? [], args.prompt, output)
|
|
3018
3251
|
+ estimateImageTokens(resolvedImages?.length ?? 0) + overhead
|
|
3019
3252
|
+ (ANSWER_GROUNDING_THINK ? ANSWER_GROUNDING_THINK_TOKENS : ANSWER_GROUNDING_OUTPUT_TOKENS)
|
|
@@ -3023,6 +3256,21 @@ export async function runInfer(args, deps) {
|
|
|
3023
3256
|
: (await (deps.groundAnswer ?? groundAnswer)({
|
|
3024
3257
|
policy, ollamaUrl: deps.ollamaUrl, model: ollamaName, messages: args.messages ?? [], prompt: args.prompt,
|
|
3025
3258
|
answer: output, images: resolvedImages?.length ? resolvedImages : undefined, numCtx: checkCtx,
|
|
3259
|
+
...(omlxTarget && omlxRuntime ? { fetchImpl: (async (_url, init) => {
|
|
3260
|
+
const body = JSON.parse(String(init?.body));
|
|
3261
|
+
const checked = await guardedOmlxGenerate(omlxRuntime, omlxTarget, {
|
|
3262
|
+
// The check's own sampling, not the caller's: temperature 0 and
|
|
3263
|
+
// the thinking setting of each attempt.
|
|
3264
|
+
messages: body.messages, maxTokens: body.options.num_predict,
|
|
3265
|
+
temperature: typeof body.options?.temperature === "number" ? body.options.temperature : 0,
|
|
3266
|
+
timeoutMs: Math.min(args.timeout_ms ?? 300_000, omlxDeadline - Date.now()),
|
|
3267
|
+
think: typeof body.think === "boolean" ? body.think : false,
|
|
3268
|
+
}, deps.freemem);
|
|
3269
|
+
return new Response(JSON.stringify(checked.ok ? {
|
|
3270
|
+
done: true, done_reason: checked.doneReason,
|
|
3271
|
+
message: { content: checked.text, thinking: checked.thinking }, prompt_eval_count: checked.promptTokens,
|
|
3272
|
+
} : { error: checked.reason }), { status: checked.ok ? 200 : 503 });
|
|
3273
|
+
}) } : {}),
|
|
3026
3274
|
}).catch(() => ({ verdict: "ERROR" }))).verdict;
|
|
3027
3275
|
}
|
|
3028
3276
|
attempts.push({ tier: tier.tag, reason: `answer_check:local_${local.toLowerCase()}` });
|
|
@@ -3085,7 +3333,7 @@ export async function runInfer(args, deps) {
|
|
|
3085
3333
|
debugLog(`[prism_infer] quality gate FAIL (${gate.reason}) — no cloud, serving local`);
|
|
3086
3334
|
}
|
|
3087
3335
|
return await applyVerification(output, gatedArgs, deps, {
|
|
3088
|
-
backend: `ollama-${tier.tag.replace("prism-coder:", "")}`,
|
|
3336
|
+
backend: omlxTarget ? "omlx" : `ollama-${tier.tag.replace("prism-coder:", "")}`,
|
|
3089
3337
|
model_picked: tier.tag,
|
|
3090
3338
|
ram_free_mb: ramFreeMb,
|
|
3091
3339
|
latency_ms: Date.now() - t0,
|
|
@@ -3295,6 +3543,13 @@ async function applyVerification(draft, args, deps, partial) {
|
|
|
3295
3543
|
*/
|
|
3296
3544
|
export function productionInferDeps() {
|
|
3297
3545
|
return {
|
|
3546
|
+
omlx: createOmlxRuntime({
|
|
3547
|
+
listLoaded: () => strictLoaded(PRISM_LOCAL_LLM_URL),
|
|
3548
|
+
unload: model => unloadOllamaModel(PRISM_LOCAL_LLM_URL, model),
|
|
3549
|
+
// Every explicit oMLX call is owned through the provider's control routes.
|
|
3550
|
+
generate: (target, request, signal) => ownedOmlxGenerate(target, request, signal),
|
|
3551
|
+
recover: target => recoverOwnedOmlx(target),
|
|
3552
|
+
}, PRISM_LOCAL_LLM_URL),
|
|
3298
3553
|
freemem: () => getAvailableMemoryBytes(),
|
|
3299
3554
|
listTags: () => listOllamaTags(PRISM_LOCAL_LLM_URL),
|
|
3300
3555
|
listLoaded: () => listOllamaLoaded(PRISM_LOCAL_LLM_URL),
|
|
@@ -3389,7 +3644,39 @@ export function inferResponseHeader(result, memory) {
|
|
|
3389
3644
|
(memory ? ` memory=${memory.project}:${memory.depth}` : "") +
|
|
3390
3645
|
(result.attempts.length ? ` attempts=${JSON.stringify(result.attempts)}` : ""));
|
|
3391
3646
|
}
|
|
3392
|
-
|
|
3647
|
+
const STRUCTURED_INFER_FIELDS = [
|
|
3648
|
+
"output", "backend", "model_picked", "ram_free_mb", "latency_ms", "used_cloud",
|
|
3649
|
+
"next_step", "plan", "history_turns", "layer1_second_read", "refusal_layer",
|
|
3650
|
+
"prompt_tokens", "completion_tokens", "quality_gate_failed", "entitlements_source",
|
|
3651
|
+
];
|
|
3652
|
+
/** Public machine contract. Never spread runtime objects, authentication,
|
|
3653
|
+
* verifier internals, or an unchecked route draft into the MCP envelope. */
|
|
3654
|
+
export function inferStructuredResult(result) {
|
|
3655
|
+
const structured = {};
|
|
3656
|
+
for (const field of STRUCTURED_INFER_FIELDS) {
|
|
3657
|
+
if (result[field] !== undefined)
|
|
3658
|
+
structured[field] = result[field];
|
|
3659
|
+
}
|
|
3660
|
+
structured.attempts = result.attempts.map(({ tier, reason }) => ({ tier, reason }));
|
|
3661
|
+
if (result.gate_outcome) {
|
|
3662
|
+
const { status, reason, served_anyway } = result.gate_outcome;
|
|
3663
|
+
structured.gate_outcome = { status, served_anyway, ...(reason === undefined ? {} : { reason }) };
|
|
3664
|
+
}
|
|
3665
|
+
if (result.multi_turn) {
|
|
3666
|
+
const { enabled, max_turns, max_chars } = result.multi_turn;
|
|
3667
|
+
structured.multi_turn = { enabled, max_turns, max_chars };
|
|
3668
|
+
}
|
|
3669
|
+
if (result.verification) {
|
|
3670
|
+
const { action, refusalClaim } = result.verification;
|
|
3671
|
+
structured.verification = { action, ...(refusalClaim === undefined ? {} : { refusalClaim }) };
|
|
3672
|
+
}
|
|
3673
|
+
if (result.route_guard) {
|
|
3674
|
+
const { action, source, reason } = result.route_guard;
|
|
3675
|
+
structured.route_guard = { action, source, ...(reason === undefined ? {} : { reason }) };
|
|
3676
|
+
}
|
|
3677
|
+
return structured;
|
|
3678
|
+
}
|
|
3679
|
+
export async function prismInferHandler(args, deps) {
|
|
3393
3680
|
if (!isPrismInferArgs(args)) {
|
|
3394
3681
|
const raw = typeof args === "object" && args !== null ? args : {};
|
|
3395
3682
|
const mp = raw.messages !== undefined ? messagesProblem(raw.messages) : null;
|
|
@@ -3402,7 +3689,7 @@ export async function prismInferHandler(args) {
|
|
|
3402
3689
|
}
|
|
3403
3690
|
try {
|
|
3404
3691
|
const prepared = await prepareMemoryAwareInferArgs(args);
|
|
3405
|
-
const result = await runInfer(prepared.args, productionInferDeps());
|
|
3692
|
+
const result = await runInfer(prepared.args, deps ?? productionInferDeps());
|
|
3406
3693
|
debugLog(`[prism_infer] backend=${result.backend} model=${result.model_picked} latency=${result.latency_ms}ms free=${result.ram_free_mb}MB`);
|
|
3407
3694
|
// Local accumulator — sole source of the user-facing metrics block.
|
|
3408
3695
|
// T4: pass prompt_text so recordInference computes submittedEst via
|
|
@@ -3447,6 +3734,7 @@ export async function prismInferHandler(args) {
|
|
|
3447
3734
|
// for the host to show the user.
|
|
3448
3735
|
...(result.next_step ? [{ type: "text", text: `Next step: ${result.next_step}` }] : []),
|
|
3449
3736
|
],
|
|
3737
|
+
structuredContent: inferStructuredResult(result),
|
|
3450
3738
|
};
|
|
3451
3739
|
}
|
|
3452
3740
|
catch (err) {
|