Files
OmniRoute/open-sse/services/compression/omniglyphTelemetry.ts
Diego Rodrigues de Sa e Souza 04af8b1517 feat(compression): adota omniglyph 1.4.0, perfis semânticos e contabilidade com evidência (#10647)
* 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>
2026-08-18 12:13:48 -03:00

157 lines
5.8 KiB
TypeScript

/**
* Ponte de telemetria do OmniGlyph — allowlist positiva.
*
* `TransformInfo` mistura contadores inofensivos com material que NUNCA pode
* ser persistido: bytes PNG, `imageSourceText(s)`, `recoverable[].text`, os
* sha8 de system/CLAUDE.md/primeira mensagem, os nomes de tags observadas e o
* bloco `env` (cwd, branch, versões). Copiar o objeto inteiro seria transformar
* a telemetria de compressão num vazamento do prompt.
*
* Este módulo não filtra por denylist — ele MONTA um objeto novo, campo a
* campo, só com número e enum. Um campo novo no upstream não entra sozinho.
*
* `normalizeAccounting()` (OmniGlyph 1.4.0) faz a parte difícil: classifica o
* grau de evidência da economia e resolve a semântica de cache por provider —
* Anthropic reporta input/cache-create/cache-read em buckets DISJUNTOS,
* enquanto OpenAI e xAI reportam `cached` como SUBCONJUNTO do input. Somar à
* mão dá double-count silencioso.
*/
import {
normalizeAccounting,
type AccountingProvider,
type OmniGlyphTransformInfo,
type SavingsEvidence,
} from "omniglyph";
/** Contabilidade segura de uma execução do OmniGlyph. Só número e enum. */
export interface OmniGlyphAccounting {
provider: AccountingProvider;
model?: string;
bytes: {
original?: number;
transformed?: number;
reduced?: number;
compressionRatio?: number;
};
tokens: {
estimatedOriginalInput?: number;
estimatedActualInput?: number;
estimatedReduced?: number;
image?: number;
};
savings: {
/** De onde saiu o número: contagem do provider, estimativa ou só bytes. */
evidence: SavingsEvidence;
inputTokensReduced?: number;
inputReductionRatio?: number;
};
images: {
count: number;
bytes: number;
pixels?: number;
};
/** Chars de origem imageados vs. mantidos como texto por turno. */
chars: {
original?: number;
imaged?: number;
static?: number;
dynamic?: number;
outgoingText?: number;
};
dynamicBlockCount?: number;
latencyMs?: number;
}
/**
* A semântica de cache de `normalizeAccounting` depende da família do provider,
* não do nome comercial da rota. Rota desconhecida vira `unknown`, que faz o
* upstream falhar fechado em vez de adivinhar buckets de cache.
*/
export function toAccountingProvider(provider: string | null | undefined): AccountingProvider {
const normalized = typeof provider === "string" ? provider.trim().toLowerCase() : "";
if (normalized === "anthropic" || normalized === "claude") return "anthropic";
if (normalized === "openai" || normalized === "codex" || normalized === "chatgpt") {
return "openai";
}
if (normalized === "xai" || normalized === "grok") return "xai";
return "unknown";
}
function count(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
}
export function buildOmniGlyphAccounting(params: {
provider: string | null | undefined;
model?: string;
originalBytes: number;
transformedBytes: number;
info?: OmniGlyphTransformInfo | null;
durationMs?: number;
}): OmniGlyphAccounting {
const { info } = params;
const provider = toAccountingProvider(params.provider);
// `baselineImagedTokens` é o custo em tokens de texto do que foi imageado (o
// "teria pago assim"); `imageTokens` é o que as imagens custam de fato. Os
// dois só existem no wire GPT — no Anthropic a evidência honesta cai para
// bytes, e é isso que o campo `evidence` passa a dizer em vez de exibir um
// número sem procedência.
const normalized = normalizeAccounting({
provider,
...(params.model ? { model: params.model } : {}),
originalBytes: params.originalBytes,
transformedBytes: params.transformedBytes,
...(count(info?.baselineImagedTokens) !== undefined
? { estimatedOriginalInputTokens: info!.baselineImagedTokens }
: {}),
...(count(info?.imageTokens) !== undefined
? { estimatedTransformedInputTokens: info!.imageTokens }
: {}),
...(count(info?.imageTokens) !== undefined ? { imageTokens: info!.imageTokens } : {}),
...(params.durationMs !== undefined ? { proxyAddedLatencyMs: params.durationMs } : {}),
});
const chars = {
...(count(info?.origChars) !== undefined ? { original: info!.origChars } : {}),
...(count(info?.compressedChars) !== undefined ? { imaged: info!.compressedChars } : {}),
...(count(info?.staticChars) !== undefined ? { static: info!.staticChars } : {}),
...(count(info?.dynamicChars) !== undefined ? { dynamic: info!.dynamicChars } : {}),
...(count(info?.outgoingTextChars) !== undefined
? { outgoingText: info!.outgoingTextChars }
: {}),
};
return {
provider: normalized.provider,
...(normalized.model ? { model: normalized.model } : {}),
bytes: normalized.bytes,
tokens: {
...(normalized.tokens.estimatedOriginalInput !== undefined
? { estimatedOriginalInput: normalized.tokens.estimatedOriginalInput }
: {}),
...(normalized.tokens.estimatedActualInput !== undefined
? { estimatedActualInput: normalized.tokens.estimatedActualInput }
: {}),
...(normalized.tokens.estimatedReduced !== undefined
? { estimatedReduced: normalized.tokens.estimatedReduced }
: {}),
...(normalized.tokens.image !== undefined ? { image: normalized.tokens.image } : {}),
},
savings: normalized.savings,
images: {
count: count(info?.imageCount) ?? 0,
bytes: count(info?.imageBytes) ?? 0,
...(count(info?.imagePixels) !== undefined ? { pixels: info!.imagePixels } : {}),
},
chars,
...(count(info?.dynamicBlockCount) !== undefined
? { dynamicBlockCount: info!.dynamicBlockCount }
: {}),
...(normalized.latency.proxyAddedMs !== undefined
? { latencyMs: normalized.latency.proxyAddedMs }
: {}),
};
}