mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 11:52:26 +03:00
* feat(compression): target-wire OmniGlyph stage and transport fidelity gate Roda o OmniGlyph depois da tradução para o wire real do provedor, em vez do corpo de origem. Um cliente OpenAI roteado para Claude deixava de comprimir com skip:source_format_not_claude porque o corpo ainda estava em formato OpenAI quando a engine era avaliada. - dispatch nativo por wire: Anthropic Messages, OpenAI Chat Completions e OpenAI Responses (input[] preservado, sem achatar para messages[]); - estágio target-wire pós-translateRequest, com guarda contra dupla compressão no caminho Claude→OpenAI; - preserveSystemPrompt do OmniRoute mapeado para compressSystem: false; - imageTransportPolicy: fidelidade de bytes/dimensões separada de supportsVision; só Anthropic/Claude tem recibo byte-preserving, o resto é fail-closed; - contagem de tokens de data URL PNG no wire OpenAI (marcador ;base64,); - README e i18n en/pt-BR com claims escopados ao caminho medido. * feat(compression): adota omniglyph 1.4.0 e tira o gate de modelo da env do host O 1.4.0 introduziu escopos de segurança e passou a resolvê-los dentro de isOmniGlyphSupportedModel() lendo process.env.OMNIGLYPH_PROFILE. Somado ao OMNIGLYPH_MODELS que já existia, duas variáveis do ambiente do host decidiam em silêncio o gate de TODO request do OmniRoute: passthrough desligaria a engine inteira e OMNIGLYPH_MODELS admitiria modelos sem recibo medido, enquanto a UI segue prometendo "Claude Fable 5 na rota direta medida". O adapter passa a usar isOmniGlyphSupportedModelForScope() com escopo explícito e fixa o escopo mais restrito como teto: a env só pode ESTREITAR a allowlist, nunca alargar. Os dois wires compartilham a mesma lista no pacote desde o 1.4.0, então uma checagem cobre Anthropic e GPT. - omniglyph ^1.3.1 -> ^1.4.0 (lock em 1.4.0); - testes de regressão para os dois caminhos de sequestro por env; - teste de contrato dos exports novos (escopo, perfis, accounting). O 1.4.0 também traz, sem mudança de código aqui: correção do glyph K que era lido como H, remoção do backtracking polinomial no secret-guard, overrides do pnpm em pnpm-workspace.yaml e as transitivas vulneráveis resolvidas. * feat(compression): expõe os perfis semânticos do omniglyph nos três wires O 1.4.0 trouxe perfis nomeados (coding-safe, balanced, aggressive, passthrough), mas só transformAnthropicMessages() os resolve sozinho: os transformadores OpenAI recebem TransformOptions cru e ignorariam o campo. Um perfil escolhido pelo operador valeria no wire Claude e sumiria no OpenAI. O adapter passa a mesclar o perfil com mergeCompressionProfileOptions() antes de chamar Chat Completions e Responses. O default segue aggressive — a política que os recibos publicados mediram. Medido nesta base: com coding-safe/balanced, uma sessão sem histórico acumulado para em below_min_chars e a engine não faz nada, porque os dois fixam minCompressChars no máximo e desligam system/tools/tool-results. Como a engine é opt-in, um default assim entregaria "ligado, 0% de ganho". O perfil é TETO, não piso: mergeCompressionProfileOptions não deixa um override do chamador reabrir uma lane lossy que o perfil fechou. Coberto por teste, por ser contra-intuitivo. Também fecha um caminho em que o OmniRoute violaria a própria política: o wire OpenAI do pacote não tem compressSystem — honra apenas compressTools, gptHistory, minCompressChars e reflow, e sempre troca a instrução por um ponteiro para a imagem. Com preserveSystemPrompt ligado, imagear assim queimaria o prefixo quente que a decisão cache-aware está protegendo, sem nada no corpo devolvido denunciando. A engine agora pula com skip:system_preservation_unsupported_on_wire. * feat(compression): contabilidade física do omniglyph com grau de evidência O adapter descartava o TransformInfo inteiro, então a UI mostrava um número de economia sem dizer de onde ele vinha — contagem do provider, estimativa ou só diferença de bytes. O 1.4.0 expõe normalizeAccounting(), que classifica essa evidência e resolve a semântica de cache por família: Anthropic reporta input, cache-create e cache-read em buckets DISJUNTOS, enquanto OpenAI e xAI reportam cached como SUBCONJUNTO do input. Somar à mão dá double-count silencioso. O novo omniglyphTelemetry.ts não filtra por denylist — MONTA um objeto novo, campo a campo, só com número e enum. TransformInfo mistura contadores inofensivos com material que não pode ser persistido: bytes PNG, imageSourceText(s), recoverable[].text, os sha8 de system/CLAUDE.md/primeira mensagem, nomes de tags observadas e o bloco env (cwd, branch, versões). Copiar o objeto inteiro transformaria telemetria de compressão em vazamento de prompt. O teste de negação prova que segredo, caminho do operador, texto do system e base64 não aparecem, e varre a allowlist exigindo que toda string seja de um enum conhecido. - provider threaded do chatCore e do bridge Codex WS até a engine; ausente vira `unknown`, que faz o upstream recusar adivinhar buckets de cache; - contabilidade propagada para o engineBreakdown do passo (o agregado do pipeline soma todas as engines e não serviria); - skip não emite contabilidade: zeros ali seriam indistinguíveis de "a engine nem rodou". * feat(compression): perfil do omniglyph configurável, persistido e documentado Fecha o caminho do operador: o perfil já existia no adapter, mas só como default de código. Agora atravessa schema Zod, normalizador do banco, API de settings e a página dedicada do engine. - OmniglyphConfig tipado + omniglyphConfigSchema (z.enum dos quatro perfis); - normalizeOmniglyphConfig: nome desconhecido vindo do storage cai para o default em vez de virar "roda com a política padrão"; - seletor na página do engine, com PATCH próprio — o perfil vive fora do mapa `engines`, e mandá-lo junto reescreveria o mapa inteiro (o store persiste o mapa como uma linha JSON só); - i18n en/pt-BR descrevendo o custo medido de cada perfil, não só o nome; - README e COMPRESSION_ENGINES.md com a regra do teto e o motivo de o default não ser o perfil mais seguro. Corrige de passagem um teste-irmão que ninguém via: o gate de transporte na UI deixou de dizer "direct Anthropic" quando os wires OpenAI nativos entraram, mas tests/unit/ui/omniglyphContextPage.test.tsx continuou afirmando a cópia antiga. O arquivo inteiro estava excluído do vitest.config.ts como "#8618 pre-existing failure", então a quebra passou silenciosa. Com a asserção alinhada o arquivo fecha 3/3, e a exclusão sai — o próprio comentário mandava removê-la quando corrigida. A doc não nomeia OMNIGLYPH_MODELS: o gate de docs fabricadas está certo em apontar que o OmniRoute nunca lê essa env — quem lê é o pacote. * fix(i18n): paridade do locale vi com as chaves novas do perfil do omniglyph `tests/unit/i18n-vi-completeness.test.ts` exige paridade ESTRITA de chaves entre en e vi — diferente do ratchet `i18n:check-ui-coverage`, que passa com 80%. As 11 chaves do seletor de perfil entraram só em en e pt-BR, e o gate de cobertura seguiu verde, então a quebra só apareceu na matriz completa do CI. --------- Co-authored-by: Xiangzhe <bakryun0718@proton.me> Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
208 lines
7.6 KiB
TypeScript
208 lines
7.6 KiB
TypeScript
// @vitest-environment jsdom
|
||
import React, { act } from "react";
|
||
import { createRoot } from "react-dom/client";
|
||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||
import { SAMPLE_METRICS } from "../../../src/app/(dashboard)/dashboard/context/omniglyph/sampleData.ts";
|
||
|
||
// i18n is not resolved in vitest/jsdom; the page hardcodes its engine copy in English
|
||
// anyway (catalog convention), so no next-intl mock is needed. Asserts are on
|
||
// i18n-independent content: headings, measured numbers, gate labels, data-testid hooks.
|
||
|
||
// ── Harness (mirrors tests/unit/ui/compressionPanel.test.tsx) ──────────────────
|
||
const containers: HTMLElement[] = [];
|
||
const roots: Array<{ unmount: () => void }> = [];
|
||
|
||
function mount(ui: React.ReactElement): HTMLElement {
|
||
const container = document.createElement("div");
|
||
document.body.appendChild(container);
|
||
containers.push(container);
|
||
const root = createRoot(container);
|
||
roots.push(root);
|
||
act(() => {
|
||
root.render(ui);
|
||
});
|
||
return container;
|
||
}
|
||
|
||
beforeEach(() => {
|
||
(
|
||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||
});
|
||
|
||
afterEach(async () => {
|
||
vi.restoreAllMocks();
|
||
await act(async () => {
|
||
while (roots.length > 0) roots.pop()?.unmount();
|
||
});
|
||
for (let i = 0; i < 10; i++) await Promise.resolve();
|
||
while (containers.length > 0) containers.pop()?.remove();
|
||
document.body.innerHTML = "";
|
||
});
|
||
|
||
async function flush() {
|
||
await act(async () => {
|
||
for (let i = 0; i < 10; i++) await Promise.resolve();
|
||
});
|
||
}
|
||
|
||
interface CapturedPut {
|
||
url: string;
|
||
body: Record<string, unknown>;
|
||
}
|
||
|
||
function setupFetchMock(): { puts: CapturedPut[] } {
|
||
const puts: CapturedPut[] = [];
|
||
const json = (body: unknown, status = 200) =>
|
||
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
|
||
|
||
// omniglyph absent from `engines` initially → disabled; rtk/caveman present so we can
|
||
// assert they SURVIVE the PUT (the store persists the whole map as one row).
|
||
const initialConfig = {
|
||
enabled: true,
|
||
engines: { rtk: { enabled: true, level: "standard" }, caveman: { enabled: false } },
|
||
};
|
||
|
||
vi.spyOn(globalThis, "fetch").mockImplementation(
|
||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||
const url = input.toString();
|
||
const method = (init?.method ?? "GET").toUpperCase();
|
||
if (url.includes("/api/settings/compression")) {
|
||
if (method === "PUT") {
|
||
const body = JSON.parse(String(init?.body ?? "{}"));
|
||
puts.push({ url, body });
|
||
return json({ ...initialConfig, ...body });
|
||
}
|
||
return json(initialConfig);
|
||
}
|
||
return json({}, 404);
|
||
}
|
||
);
|
||
return { puts };
|
||
}
|
||
|
||
// ── Tests ──────────────────────────────────────────────────────────────────────
|
||
describe("OmniglyphContextPage", () => {
|
||
it("renders the four sections with the measured numbers and the real render", async () => {
|
||
setupFetchMock();
|
||
const { default: Page } = await import(
|
||
"../../../src/app/(dashboard)/dashboard/context/omniglyph/OmniglyphContextPageClient"
|
||
);
|
||
let container!: HTMLElement;
|
||
await act(async () => {
|
||
container = mount(<Page />);
|
||
});
|
||
await flush();
|
||
|
||
const text = container.textContent ?? "";
|
||
// Header + preview status
|
||
expect(text).toContain("OmniGlyph");
|
||
expect(text).toContain("Preview");
|
||
// Economics
|
||
expect(text).toContain("~10×");
|
||
expect(text).toContain("59–70%");
|
||
// Before → after: measured savings + the real rendered image
|
||
expect(text).toContain(`−${SAMPLE_METRICS.savingsPct}% tokens on this block`);
|
||
const img = container.querySelector("img");
|
||
expect(img, "the rendered sample page <img> must be present").toBeTruthy();
|
||
expect(img!.getAttribute("src")).toMatch(/^data:image\/png;base64,/);
|
||
// Gates
|
||
expect(text).toContain("claude-fable-5");
|
||
// A cópia do gate deixou de dizer "direct Anthropic" quando os wires OpenAI
|
||
// nativos entraram: transporte direto é condição de TODO provider, e o que
|
||
// restringe a rota ao Anthropic é o recibo de fidelidade de bytes, não o
|
||
// rótulo do transporte.
|
||
expect(text).toContain("direct provider");
|
||
// Config control
|
||
expect(container.querySelector('[data-testid="omniglyph-enable-toggle"]')).toBeTruthy();
|
||
});
|
||
|
||
it("enabling the engine PUTs the full engines map with omniglyph on, preserving the others", async () => {
|
||
const { puts } = setupFetchMock();
|
||
const { default: Page } = await import(
|
||
"../../../src/app/(dashboard)/dashboard/context/omniglyph/OmniglyphContextPageClient"
|
||
);
|
||
let container!: HTMLElement;
|
||
await act(async () => {
|
||
container = mount(<Page />);
|
||
});
|
||
await flush();
|
||
|
||
const toggle = container.querySelector('[data-testid="omniglyph-enable-toggle"] button') as HTMLButtonElement | null;
|
||
expect(toggle, "enable toggle button must exist").toBeTruthy();
|
||
await act(async () => {
|
||
toggle!.click();
|
||
});
|
||
await flush();
|
||
|
||
expect(puts.length).toBe(1);
|
||
const engines = puts[0]!.body.engines as Record<string, { enabled: boolean }>;
|
||
expect(engines.omniglyph).toEqual({ enabled: true });
|
||
// The other engines must survive the whole-map PUT.
|
||
expect(engines.rtk?.enabled).toBe(true);
|
||
expect(engines.caveman?.enabled).toBe(false);
|
||
});
|
||
|
||
it("carrega o perfil salvo e faz PATCH só do perfil, sem reescrever o mapa de engines", async () => {
|
||
const puts: CapturedPut[] = [];
|
||
const json = (body: unknown, status = 200) =>
|
||
new Response(JSON.stringify(body), {
|
||
status,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
const stored = {
|
||
enabled: true,
|
||
engines: { rtk: { enabled: true, level: "standard" } },
|
||
omniglyph: { profile: "coding-safe" },
|
||
};
|
||
vi.spyOn(globalThis, "fetch").mockImplementation(
|
||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||
const url = input.toString();
|
||
const method = (init?.method ?? "GET").toUpperCase();
|
||
if (url.includes("/api/settings/compression")) {
|
||
if (method === "PUT") {
|
||
puts.push({ url, body: JSON.parse(String(init?.body ?? "{}")) });
|
||
return json(stored);
|
||
}
|
||
return json(stored);
|
||
}
|
||
return json({}, 404);
|
||
}
|
||
);
|
||
|
||
const { default: Page } = await import(
|
||
"../../../src/app/(dashboard)/dashboard/context/omniglyph/OmniglyphContextPageClient"
|
||
);
|
||
let container!: HTMLElement;
|
||
await act(async () => {
|
||
container = mount(<Page />);
|
||
});
|
||
await flush();
|
||
|
||
const select = container.querySelector(
|
||
'[data-testid="omniglyph-profile-select"]'
|
||
) as HTMLSelectElement | null;
|
||
expect(select, "o seletor de perfil deve existir").toBeTruthy();
|
||
expect(select!.value, "o perfil salvo tem de vir selecionado").toBe("coding-safe");
|
||
|
||
// Os quatro perfis do pacote, com aggressive como primeiro (default).
|
||
expect(Array.from(select!.options).map((o) => o.value)).toEqual([
|
||
"aggressive",
|
||
"balanced",
|
||
"coding-safe",
|
||
"passthrough",
|
||
]);
|
||
|
||
await act(async () => {
|
||
select!.value = "passthrough";
|
||
select!.dispatchEvent(new Event("change", { bubbles: true }));
|
||
});
|
||
await flush();
|
||
|
||
expect(puts.length).toBe(1);
|
||
expect(puts[0]!.body).toEqual({ omniglyph: { profile: "passthrough" } });
|
||
// O perfil vive fora do mapa `engines`: mandá-lo junto reescreveria o mapa inteiro.
|
||
expect(puts[0]!.body.engines).toBeUndefined();
|
||
});
|
||
});
|