mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 03:42:21 +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.
258 lines
8.2 KiB
TypeScript
258 lines
8.2 KiB
TypeScript
import { isVisionModelId } from "@/shared/constants/visionModels";
|
|
import type { CompressionResult, CompressionMode } from "./types.ts";
|
|
import { createCompressionStats } from "./stats.ts";
|
|
|
|
interface Message {
|
|
role: string;
|
|
content: string | Array<Record<string, unknown>>;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface ChatBody {
|
|
messages?: Message[];
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface LiteCompressionOptions {
|
|
model?: string;
|
|
supportsVision?: boolean | null;
|
|
preserveSystemPrompt?: boolean;
|
|
compressToolResults?: boolean;
|
|
}
|
|
|
|
function normalizeMessageWhitespace(content: string): string {
|
|
if (!content) return "";
|
|
return content.replace(/\n{3,}/g, "\n\n").replace(/[ \t]+$/gm, "");
|
|
}
|
|
|
|
// Vision detection is centralized in `@/shared/constants/visionModels` (#4072) so
|
|
// the lite image-strip gate, the /v1/models listing, and the routing fallback can
|
|
// never disagree. The shared list keeps the #3328 MiniMax M3 carve-out and the
|
|
// pixtral/llava/qwen-vl/glm-4v/kimi-vl/mistral-medium-3 families this gate used to
|
|
// miss (stripping their images and blinding real vision models).
|
|
function modelSupportsVision(model: string): boolean {
|
|
return isVisionModelId(model);
|
|
}
|
|
|
|
export function collapseWhitespace(
|
|
body: ChatBody,
|
|
options: LiteCompressionOptions = {}
|
|
): {
|
|
body: ChatBody;
|
|
applied: boolean;
|
|
} {
|
|
if (!body.messages) return { body, applied: false };
|
|
let applied = false;
|
|
const messages = body.messages.map((msg) => {
|
|
if (options.preserveSystemPrompt === true && msg.role === "system") return msg;
|
|
if (typeof msg.content !== "string") return msg;
|
|
const normalized = normalizeMessageWhitespace(msg.content);
|
|
if (normalized !== msg.content) applied = true;
|
|
return { ...msg, content: normalized };
|
|
});
|
|
return { body: { ...body, messages }, applied };
|
|
}
|
|
|
|
export function dedupSystemPrompt(
|
|
body: ChatBody,
|
|
options: LiteCompressionOptions = {}
|
|
): {
|
|
body: ChatBody;
|
|
applied: boolean;
|
|
} {
|
|
if (!body.messages) return { body, applied: false };
|
|
if (options.preserveSystemPrompt === true) return { body, applied: false };
|
|
const seen = new Set<string>();
|
|
let applied = false;
|
|
const messages = body.messages.filter((msg) => {
|
|
if (msg.role !== "system" || typeof msg.content !== "string") return true;
|
|
const key = msg.content.trim().slice(0, 200);
|
|
if (seen.has(key)) {
|
|
applied = true;
|
|
return false;
|
|
}
|
|
seen.add(key);
|
|
return true;
|
|
});
|
|
return { body: { ...body, messages }, applied };
|
|
}
|
|
|
|
// Adjust a hard cut index to the nearest whitespace within a small lookback/lookahead
|
|
// window, so a truncated tool result never garbles the word it lands in the middle of
|
|
// (#8169). Prefers backing off to the end of the previous word (keeps the result at or
|
|
// under the limit); if no whitespace precedes the cut within the window (e.g. the tail
|
|
// end of a very long unbroken run), looks forward to complete the current word instead.
|
|
// Falls back to the original hard cut index when neither direction finds a boundary.
|
|
const TOOL_TRUNCATION_LOOKBACK = 80;
|
|
|
|
function isWordChar(char: string | undefined): boolean {
|
|
return char !== undefined && /\S/.test(char);
|
|
}
|
|
|
|
function findWhitespaceBackward(content: string, cutIndex: number): number {
|
|
const windowStart = Math.max(0, cutIndex - TOOL_TRUNCATION_LOOKBACK);
|
|
for (let i = cutIndex; i > windowStart; i--) {
|
|
if (!isWordChar(content[i - 1])) return i - 1;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function findWhitespaceForward(content: string, cutIndex: number): number {
|
|
const windowEnd = Math.min(content.length, cutIndex + TOOL_TRUNCATION_LOOKBACK);
|
|
for (let i = cutIndex; i < windowEnd; i++) {
|
|
if (!isWordChar(content[i])) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function backOffToWordBoundary(content: string, cutIndex: number): number {
|
|
const onWordBoundary = !isWordChar(content[cutIndex - 1]) || !isWordChar(content[cutIndex]);
|
|
if (onWordBoundary) return cutIndex;
|
|
|
|
const backward = findWhitespaceBackward(content, cutIndex);
|
|
if (backward !== -1) return backward;
|
|
|
|
const forward = findWhitespaceForward(content, cutIndex);
|
|
if (forward !== -1) return forward;
|
|
|
|
return cutIndex;
|
|
}
|
|
|
|
export function compressToolResults(body: ChatBody): {
|
|
body: ChatBody;
|
|
applied: boolean;
|
|
} {
|
|
if (!body.messages) return { body, applied: false };
|
|
const MAX_TOOL_LENGTH = 2000;
|
|
let applied = false;
|
|
const messages = body.messages.map((msg) => {
|
|
if (msg.role !== "tool" || typeof msg.content !== "string") return msg;
|
|
if (msg.content.length <= MAX_TOOL_LENGTH) return msg;
|
|
applied = true;
|
|
const cutIndex = backOffToWordBoundary(msg.content, MAX_TOOL_LENGTH);
|
|
return {
|
|
...msg,
|
|
content: msg.content.slice(0, cutIndex) + "\n...[truncated]",
|
|
};
|
|
});
|
|
return { body: { ...body, messages }, applied };
|
|
}
|
|
|
|
export function removeRedundantContent(
|
|
body: ChatBody,
|
|
options: LiteCompressionOptions = {}
|
|
): {
|
|
body: ChatBody;
|
|
applied: boolean;
|
|
} {
|
|
if (!body.messages) return { body, applied: false };
|
|
let applied = false;
|
|
const messages: Message[] = [];
|
|
for (let i = 0; i < body.messages.length; i++) {
|
|
const msg = body.messages[i];
|
|
if (options.preserveSystemPrompt === true && msg.role === "system") {
|
|
messages.push(msg);
|
|
continue;
|
|
}
|
|
const contentStr = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
if (
|
|
i > 0 &&
|
|
body.messages[i - 1].role === msg.role &&
|
|
typeof body.messages[i - 1].content === "string" &&
|
|
body.messages[i - 1].content === contentStr
|
|
) {
|
|
applied = true;
|
|
continue;
|
|
}
|
|
messages.push(msg);
|
|
}
|
|
return { body: { ...body, messages }, applied };
|
|
}
|
|
|
|
export function replaceImageUrls(
|
|
body: ChatBody,
|
|
options?: LiteCompressionOptions | string
|
|
): { body: ChatBody; applied: boolean } {
|
|
if (!body.messages) return { body, applied: false };
|
|
const supportsVision =
|
|
typeof options === "object" && options !== null
|
|
? options.supportsVision
|
|
: typeof options === "string"
|
|
? modelSupportsVision(options)
|
|
: undefined;
|
|
if (supportsVision !== false) return { body, applied: false };
|
|
|
|
let applied = false;
|
|
const messages = body.messages.map((msg) => {
|
|
if (!Array.isArray(msg.content)) return msg;
|
|
const newContent = msg.content.map((part) => {
|
|
if (
|
|
typeof part === "object" &&
|
|
part !== null &&
|
|
part.type === "image_url" &&
|
|
typeof (part as Record<string, unknown>).image_url === "object" &&
|
|
((part as Record<string, unknown>).image_url as Record<string, unknown>)?.url
|
|
) {
|
|
const url = String(
|
|
((part as Record<string, unknown>).image_url as Record<string, unknown>).url
|
|
);
|
|
if (url.startsWith("data:image/")) {
|
|
applied = true;
|
|
const format = url.slice(url.indexOf("/") + 1, url.indexOf(";")) || "unknown";
|
|
return { type: "text", text: `[image: ${format}]` };
|
|
}
|
|
}
|
|
return part;
|
|
});
|
|
return { ...msg, content: newContent };
|
|
});
|
|
return { body: { ...body, messages }, applied };
|
|
}
|
|
|
|
export function applyLiteCompression(
|
|
body: Record<string, unknown>,
|
|
options?: LiteCompressionOptions
|
|
): CompressionResult {
|
|
const originalBody = body;
|
|
let current = body as ChatBody;
|
|
const techniquesApplied: string[] = [];
|
|
|
|
const r1 = collapseWhitespace(current, options);
|
|
current = r1.body;
|
|
if (r1.applied) techniquesApplied.push("whitespace");
|
|
|
|
const r2 = dedupSystemPrompt(current, options);
|
|
current = r2.body;
|
|
if (r2.applied) techniquesApplied.push("system-dedup");
|
|
|
|
if (options?.compressToolResults !== false) {
|
|
const r3 = compressToolResults(current);
|
|
current = r3.body;
|
|
if (r3.applied) techniquesApplied.push("tool-compress");
|
|
}
|
|
|
|
const r4 = removeRedundantContent(current, options);
|
|
current = r4.body;
|
|
if (r4.applied) techniquesApplied.push("redundant-remove");
|
|
|
|
const r5 = replaceImageUrls(current, options);
|
|
current = r5.body;
|
|
if (r5.applied) techniquesApplied.push("image-placeholder");
|
|
|
|
const compressed = techniquesApplied.length > 0;
|
|
const stats = compressed
|
|
? createCompressionStats(
|
|
originalBody,
|
|
current as Record<string, unknown>,
|
|
"lite" as CompressionMode,
|
|
techniquesApplied
|
|
)
|
|
: null;
|
|
|
|
return {
|
|
body: current as Record<string, unknown>,
|
|
compressed,
|
|
stats,
|
|
};
|
|
}
|