From ce89143232df98dae0bf7a7700032a86ecbd9117 Mon Sep 17 00:00:00 2001 From: Xiangzhe Date: Tue, 18 Aug 2026 00:04:26 -0300 Subject: [PATCH] feat(compression): adota omniglyph 1.4.0 e tira o gate de modelo da env do host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../compression/engines/omniglyphAdapter.ts | 38 +++++++++++++---- package-lock.json | 8 ++-- package.json | 12 +++--- .../compression/omniglyph-adapter.test.ts | 42 +++++++++++++++++++ .../unit/compression/omniglyph-import.test.ts | 27 ++++++++++++ 5 files changed, 110 insertions(+), 17 deletions(-) diff --git a/open-sse/services/compression/engines/omniglyphAdapter.ts b/open-sse/services/compression/engines/omniglyphAdapter.ts index b076234972..4e18fa7dbe 100644 --- a/open-sse/services/compression/engines/omniglyphAdapter.ts +++ b/open-sse/services/compression/engines/omniglyphAdapter.ts @@ -21,15 +21,41 @@ import type { CompressionEngine, CompressionEngineApplyOptions } from "./types.t import type { CompressionResult } from "../types.ts"; import { createCompressionStats } from "../stats.ts"; import { - isOmniGlyphSupportedGptModel, - isOmniGlyphSupportedModel, + isOmniGlyphSupportedModelForScope, transformAnthropicMessages, transformOpenAIChatCompletions, transformOpenAIResponses, transformRequest, + type OmniGlyphSafetyScope, } from "omniglyph"; import { isModelImageable } from "omniglyph/applicability"; +/** + * Teto de modelos do OmniRoute — sempre o escopo mais restrito do pacote. + * + * `isOmniGlyphSupportedModel()` resolve o escopo lendo `OMNIGLYPH_PROFILE` do + * processo, e a lista base sai de `OMNIGLYPH_MODELS`. Duas variáveis do HOST + * decidiriam, 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 continua prometendo "Claude Fable 5 na rota + * direta medida". Fixar o escopo mais restrito faz o gate só poder ESTREITAR + * pela env, nunca alargar, e mantém a decisão na configuração do OmniRoute. + */ +const MEASURED_MODEL_SCOPE: OmniGlyphSafetyScope = "coding-safe"; + +/** Escopo semântico do transform. `aggressive` mantém a política medida atual. */ +const DEFAULT_TRANSFORM_SCOPE: OmniGlyphSafetyScope = "aggressive"; + +/** + * O modelo precisa passar no teto medido E no escopo em vigor. Os dois wires + * (Anthropic e GPT) compartilham a mesma allowlist no pacote desde 1.4.0, então + * uma única checagem cobre os dois. + */ +function isModelWithinScope(model: string, scope: OmniGlyphSafetyScope): boolean { + if (!isOmniGlyphSupportedModelForScope(model, MEASURED_MODEL_SCOPE)) return false; + return isOmniGlyphSupportedModelForScope(model, scope); +} + function skip(body: Record, reason: string): CompressionResult { try { return { @@ -133,11 +159,9 @@ async function applyOmniglyph( ) { return skip(body, "source_format_not_openai_responses"); } - const supportedModel = - wireFormat === "claude" - ? isOmniGlyphSupportedModel(model) - : isOmniGlyphSupportedGptModel(model); - if (!supportedModel) return skip(body, "model_not_approved"); + if (!isModelWithinScope(model, DEFAULT_TRANSFORM_SCOPE)) { + return skip(body, "model_not_approved"); + } // OmniGlyph 1.3.x deliberately keeps unverified families (currently Grok) // text-only until the operator acknowledges them via its own env gate. if (!isModelImageable(model)) return skip(body, "model_not_imageable"); diff --git a/package-lock.json b/package-lock.json index 71fd7e0093..1e46ee388b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "next-intl": "^4.13.6", "next-themes": "^0.4.6", "node-machine-id": "^1.1.12", - "omniglyph": "^1.3.1", + "omniglyph": "^1.4.0", "onnxruntime-node": "~1.24.3", "open": "^11.0.0", "ora": "^9.4.1", @@ -28871,9 +28871,9 @@ "license": "MIT" }, "node_modules/omniglyph": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/omniglyph/-/omniglyph-1.3.1.tgz", - "integrity": "sha512-6QnZCoXYczjsPN2x+XpbimimjO6kCoSZUzsdSvoKjtw28U1U724VgLICBNaLX4FFs5jd7SrYNNs9Aee2iIkcoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/omniglyph/-/omniglyph-1.4.0.tgz", + "integrity": "sha512-4zAqDW9pBb2i+fiGOVLIKbdecZeo55UmKQoLku1apxo3TSA4gfqcMo2MqQpal1VicckUwTbexFLasW7qngXUHA==", "license": "MIT", "dependencies": { "gpt-tokenizer": "^3.4.0" diff --git a/package.json b/package.json index 94a6ec3705..38f0543059 100644 --- a/package.json +++ b/package.json @@ -263,6 +263,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@huggingface/transformers": "^4.2.0", "@lobehub/icons": "^5.8.0", "@modelcontextprotocol/sdk": "^1.29.0", "@monaco-editor/react": "^4.7.0", @@ -304,7 +305,8 @@ "next-intl": "^4.13.6", "next-themes": "^0.4.6", "node-machine-id": "^1.1.12", - "omniglyph": "^1.3.1", + "omniglyph": "^1.4.0", + "onnxruntime-node": "~1.24.3", "open": "^11.0.0", "ora": "^9.4.1", "parse5": "^8.0.1", @@ -335,9 +337,7 @@ "xxhash-wasm": "^1.1.0", "yazl": "^3.3.1", "zod": "^4.4.3", - "zustand": "^5.0.13", - "@huggingface/transformers": "^4.2.0", - "onnxruntime-node": "~1.24.3" + "zustand": "^5.0.13" }, "optionalDependencies": { "@atjsh/llmlingua-2": "2.0.3", @@ -345,9 +345,9 @@ "better-sqlite3": "^13.0.2", "js-tiktoken": "^1.0.20", "keytar": "^7.9.0", + "sqlite-vec": "^0.1.9", "tls-client-node": "^0.2.0", - "wreq-js": "^3.0.0", - "sqlite-vec": "^0.1.9" + "wreq-js": "^3.0.0" }, "devDependencies": { "@axe-core/playwright": "^4.11.3", diff --git a/tests/unit/compression/omniglyph-adapter.test.ts b/tests/unit/compression/omniglyph-adapter.test.ts index c463df2603..5e76777445 100644 --- a/tests/unit/compression/omniglyph-adapter.test.ts +++ b/tests/unit/compression/omniglyph-adapter.test.ts @@ -187,3 +187,45 @@ test("OpenAI não roda no estágio pré-tradução", async () => { assert.equal(r.compressed, false); assert.ok(r.stats?.techniquesUsed.includes("skip:requires_post_translation")); }); + +// OmniGlyph 1.4.0 introduziu escopos de segurança (`coding-safe`/`balanced`/ +// `aggressive`/`passthrough`) e passou a resolvê-los, dentro de +// `isOmniGlyphSupportedModel()`, lendo `process.env.OMNIGLYPH_PROFILE`. Isso +// transforma uma variável de ambiente do HOST num gate silencioso de TODO +// request do OmniRoute: um `OMNIGLYPH_PROFILE=passthrough` exportado no shell +// do processo desligaria a engine sem que nenhuma configuração do OmniRoute +// tivesse mudado — e sem nenhum sinal na UI. A política é do OmniRoute; o +// adapter tem de passar o escopo explicitamente. +async function withEnv(key: string, value: string, fn: () => Promise): Promise { + const had = Object.prototype.hasOwnProperty.call(process.env, key); + const previous = process.env[key]; + process.env[key] = value; + try { + return await fn(); + } finally { + if (had) process.env[key] = previous; + else delete process.env[key]; + } +} + +test("OMNIGLYPH_PROFILE do host não decide o gate de modelo do OmniRoute", async () => { + const r = await withEnv("OMNIGLYPH_PROFILE", "passthrough", () => + omniglyphEngine.applyAsync!(claudeBody(), OK) + ); + assert.equal( + r.compressed, + true, + "env do processo não pode desligar a engine: o escopo vem da config do OmniRoute" + ); +}); + +test("OMNIGLYPH_PROFILE do host não amplia a allowlist de modelos do OmniRoute", async () => { + const body = { ...claudeBody(), model: "claude-sonnet-5" }; + const r = await withEnv("OMNIGLYPH_PROFILE", "aggressive", () => + withEnv("OMNIGLYPH_MODELS", "claude-fable-5,claude-sonnet-5", () => + omniglyphEngine.applyAsync!(body, { ...OK, model: "claude-sonnet-5" }) + ) + ); + assert.equal(r.compressed, false, "modelo sem recibo medido não pode entrar via env do host"); + assert.ok(r.stats?.techniquesUsed.includes("skip:model_not_approved")); +}); diff --git a/tests/unit/compression/omniglyph-import.test.ts b/tests/unit/compression/omniglyph-import.test.ts index 170d6bf105..04014451b5 100644 --- a/tests/unit/compression/omniglyph-import.test.ts +++ b/tests/unit/compression/omniglyph-import.test.ts @@ -15,3 +15,30 @@ test("pacote omniglyph exporta a API que o adapter consome", async () => { const applicability = await import("omniglyph/applicability"); assert.equal(typeof applicability.isModelImageable, "function"); }); + +// Superfícies introduzidas no 1.4.0. Sem esta asserção, uma remoção upstream só +// apareceria em runtime — o adapter importa esses símbolos diretamente. +test("omniglyph 1.4.0 exporta escopo de segurança, perfis e accounting", async () => { + const mod = await import("omniglyph"); + + // Gate de modelo por escopo explícito (não pela env do processo). + assert.equal(typeof mod.isOmniGlyphSupportedModelForScope, "function"); + assert.equal(mod.isOmniGlyphSupportedModelForScope("claude-fable-5", "coding-safe"), true); + assert.equal(mod.isOmniGlyphSupportedModelForScope("claude-sonnet-5", "coding-safe"), false); + assert.equal( + mod.isOmniGlyphSupportedModelForScope("claude-fable-5", "passthrough"), + false, + "passthrough não habilita modelo nenhum" + ); + + // Perfis semânticos. + assert.equal(typeof mod.resolveCompressionProfile, "function"); + assert.equal(typeof mod.mergeCompressionProfileOptions, "function"); + assert.equal(typeof mod.shouldKeepToolResultSharp, "function"); + assert.equal(mod.resolveCompressionProfile("coding-safe").name, "coding-safe"); + assert.throws(() => mod.resolveCompressionProfile("nao-existe")); + + // Contabilidade física normalizada. + assert.equal(typeof mod.normalizeAccounting, "function"); + assert.equal(typeof mod.providerActualInputTokens, "function"); +});