mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 11:22:15 +03:00
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host. Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean. Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
126 lines
4.2 KiB
TypeScript
126 lines
4.2 KiB
TypeScript
/**
|
|
* Fast object-tree size estimator — walks without JSON.stringify / toJSON / clone.
|
|
* Safe for circular references (WeakSet). Iterative frames only (no recursive call stack).
|
|
*
|
|
* Budgets:
|
|
* - byteLimit param (default ESTIMATE_SIZE_BYTE_LIMIT, 256 KiB): early-exit
|
|
* once counted bytes exceed the limit — pass the caller's own threshold
|
|
* explicitly rather than relying on the default, since a caller comparing
|
|
* against a bigger configured limit would otherwise never see a size
|
|
* above 256 KiB.
|
|
* - ESTIMATE_SIZE_NODE_BUDGET: max value visits (containers + primitives/elements)
|
|
*
|
|
* Arrays are walked by index frame (never pre-push/copy every element reference).
|
|
* Plain objects yield own enumerable values incrementally (no Object.keys materialization).
|
|
* Node-budget exhaustion returns a value strictly above the effective byteLimit
|
|
* so callers fail closed.
|
|
*/
|
|
|
|
/** Default byte early-exit threshold (256 KiB) when a caller doesn't pass its own. */
|
|
export const ESTIMATE_SIZE_BYTE_LIMIT = 262_144;
|
|
|
|
/**
|
|
* Max value/element visits before fail-closed.
|
|
* Conservative cap keeps auxiliary stack/WeakSet growth bounded under adversarial input.
|
|
*/
|
|
export const ESTIMATE_SIZE_NODE_BUDGET = 16_384;
|
|
|
|
type Frame =
|
|
| { t: "v"; v: unknown }
|
|
| { t: "a"; a: unknown[]; i: number }
|
|
| { t: "o"; o: object; it: Iterator<string> };
|
|
|
|
function ownEnumerableKeyIterator(obj: object): Iterator<string> {
|
|
return (function* ownEnumerableKeys() {
|
|
for (const key in obj) {
|
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
yield key;
|
|
}
|
|
}
|
|
})();
|
|
}
|
|
|
|
/** @returns next byte total, or a value > limit when the limit is exceeded. */
|
|
function addPrimitiveBytes(bytes: number, v: string | number | boolean): number {
|
|
if (typeof v === "string") return bytes + v.length;
|
|
if (typeof v === "number") return bytes + 8;
|
|
return bytes + 4;
|
|
}
|
|
|
|
function enqueueContainer(stack: Frame[], obj: object, seen: WeakSet<object>): void {
|
|
if (seen.has(obj)) return;
|
|
seen.add(obj);
|
|
if (Array.isArray(obj)) {
|
|
if (obj.length > 0) stack.push({ t: "a", a: obj, i: 0 });
|
|
return;
|
|
}
|
|
stack.push({ t: "o", o: obj, it: ownEnumerableKeyIterator(obj) });
|
|
}
|
|
|
|
type ValueFrame = Extract<Frame, { t: "v" }>;
|
|
|
|
function isValueFrame(frame: Frame): frame is ValueFrame {
|
|
return frame.t === "v";
|
|
}
|
|
|
|
/** Expand a container frame into the next child value. */
|
|
function expandContainerFrame(stack: Frame[], frame: Exclude<Frame, ValueFrame>): void {
|
|
if (frame.t === "a") {
|
|
if (frame.i >= frame.a.length) return;
|
|
if (frame.i + 1 < frame.a.length) {
|
|
stack.push({ t: "a", a: frame.a, i: frame.i + 1 });
|
|
}
|
|
stack.push({ t: "v", v: frame.a[frame.i] });
|
|
return;
|
|
}
|
|
const next = frame.it.next();
|
|
if (next.done) return;
|
|
stack.push(frame);
|
|
stack.push({ t: "v", v: (frame.o as Record<string, unknown>)[next.value] });
|
|
}
|
|
|
|
/**
|
|
* @param byteLimit - early-exit threshold (default ESTIMATE_SIZE_BYTE_LIMIT,
|
|
* 256 KiB). Pass the actual threshold you're comparing against (see
|
|
* chatCore/logTruncation.ts::truncateForLog) so raising that threshold
|
|
* doesn't silently cap what this function is even capable of reporting —
|
|
* the byte check and the node-budget fail-closed fallback both key off this
|
|
* value, not the fixed module constant, when a caller supplies one.
|
|
*/
|
|
export function estimateSizeFast(value: unknown, byteLimit = ESTIMATE_SIZE_BYTE_LIMIT): number {
|
|
let bytes = 0;
|
|
let visitsLeft = ESTIMATE_SIZE_NODE_BUDGET;
|
|
const seen = new WeakSet<object>();
|
|
const stack: Frame[] = [{ t: "v", v: value }];
|
|
|
|
while (stack.length > 0) {
|
|
if (visitsLeft <= 0) return byteLimit + 1;
|
|
|
|
const frame = stack.pop()!;
|
|
if (!isValueFrame(frame)) {
|
|
expandContainerFrame(stack, frame);
|
|
continue;
|
|
}
|
|
|
|
visitsLeft -= 1;
|
|
const v = frame.v;
|
|
if (v === null || v === undefined) continue;
|
|
|
|
const ty = typeof v;
|
|
if (ty === "string" || ty === "number" || ty === "boolean") {
|
|
bytes = addPrimitiveBytes(bytes, v as string | number | boolean);
|
|
if (bytes > byteLimit) return bytes;
|
|
continue;
|
|
}
|
|
if (ty === "object") {
|
|
enqueueContainer(stack, v as object, seen);
|
|
}
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
export function isSmallEnoughForSemanticCache(value: unknown): boolean {
|
|
return estimateSizeFast(value) <= 256 * 1024;
|
|
}
|