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".
This commit is contained in:
Xiangzhe
2026-08-18 00:23:47 -03:00
parent b5e5838a59
commit 638fe783a0
10 changed files with 356 additions and 12 deletions

View File

@@ -1552,6 +1552,10 @@ export async function handleChatCore({
// OmniGlyph uses a measured provider/image-fidelity allowlist. Direct HTTP
// alone is not proof that a route preserves PNG bytes and dimensions.
...resolveOmniGlyphTransport(provider),
// Sem o provider, a contabilidade do OmniGlyph cai para `unknown` e
// recusa deduzir a semântica de cache (Anthropic usa buckets disjuntos,
// OpenAI reporta cached como subconjunto do input).
provider,
sourceFormat,
targetFormat,
compressionStage: "pre-translation" as const,

View File

@@ -20,6 +20,10 @@
import type { CompressionEngine, CompressionEngineApplyOptions } from "./types.ts";
import type { CompressionResult } from "../types.ts";
import { createCompressionStats } from "../stats.ts";
import {
buildOmniGlyphAccounting,
type OmniGlyphAccounting,
} from "../omniglyphTelemetry.ts";
import {
isOmniGlyphSupportedModelForScope,
mergeCompressionProfileOptions,
@@ -209,6 +213,7 @@ async function applyOmniglyph(
if (!isModelImageable(model)) return skip(body, "model_not_imageable");
const started = Date.now();
let outBody: Record<string, unknown>;
let accounting: OmniGlyphAccounting | undefined;
try {
// The upstream OpenAI transformer resolves its billing/render profile from
// body.model. Keep the provider body byte-compatible on output, but use the
@@ -235,24 +240,34 @@ async function applyOmniglyph(
if (!applied) return skip(body, result.info?.reason ?? "not_profitable");
outBody = JSON.parse(new TextDecoder().decode(result.body)) as Record<string, unknown>;
if (transformBody !== body && body.model !== undefined) outBody.model = body.model;
accounting = buildOmniGlyphAccounting({
provider: options?.provider,
model,
originalBytes: encoded.byteLength,
transformedBytes: result.body.byteLength,
info: result.info,
durationMs: Date.now() - started,
});
} catch {
// Fail-open: qualquer erro no encode/transform/decode (ex.: corpo não serializável,
// render PNG estourando, JSON decodificado malformado) vira skip, nunca propaga.
return skip(body, "transform_error");
}
return {
body: outBody,
compressed: true,
stats: createCompressionStats(
body,
outBody,
"stacked",
["omniglyph:context-as-image"],
undefined,
Date.now() - started
),
};
const stats = createCompressionStats(
body,
outBody,
"stacked",
["omniglyph:context-as-image"],
undefined,
Date.now() - started
);
// A contabilidade só acompanha uma conversão que realmente aconteceu: um skip
// não tem economia para reportar, e inventar zeros ali viraria "0% de ganho"
// indistinguível de "a engine nem rodou".
if (accounting) stats.omniglyph = accounting;
return { body: outBody, compressed: true, stats };
}
export const omniglyphEngine: CompressionEngine = {

View File

@@ -61,6 +61,10 @@ export interface CompressionEngineApplyOptions {
stepConfig?: Record<string, unknown>;
/** Authenticated principal (API key id) making the request. Used by CCR to scope its store. */
principalId?: string;
/** Provider resolvido do alvo. A contabilidade do omniglyph depende dele:
* Anthropic reporta input/cache em buckets disjuntos, OpenAI/xAI reportam
* cached como subconjunto do input. Ausente => `unknown` (falha fechado). */
provider?: string;
}
export interface CompressionEngine {

View File

@@ -0,0 +1,156 @@
/**
* 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 }
: {}),
};
}

View File

@@ -110,5 +110,8 @@ export function mergeStackStep(
techniquesUsed: result.stats.techniquesUsed,
...(result.stats.rulesApplied ? { rulesApplied: result.stats.rulesApplied } : {}),
...(result.stats.durationMs !== undefined ? { durationMs: result.stats.durationMs } : {}),
// O agregado do pipeline soma tokens de todas as engines; a contabilidade
// física do omniglyph só faz sentido no passo que a produziu.
...(result.stats.omniglyph ? { omniglyph: result.stats.omniglyph } : {}),
});
}

View File

@@ -482,6 +482,8 @@ export async function applyCompressionAsync(
supportsVision?: boolean | null;
/** Direct-to-provider vs. aggregator transport (gates transport-sensitive engines like omniglyph). */
providerTransport?: "direct" | "aggregator";
/** Provider resolvido — a contabilidade do omniglyph depende dele. */
provider?: string;
imageTransportFidelity?: ImageTransportFidelity;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
@@ -505,6 +507,8 @@ async function runCompressionAsync(
supportsVision?: boolean | null;
/** Direct-to-provider vs. aggregator transport (gates transport-sensitive engines like omniglyph). */
providerTransport?: "direct" | "aggregator";
/** Provider resolvido — a contabilidade do omniglyph depende dele. */
provider?: string;
imageTransportFidelity?: ImageTransportFidelity;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
@@ -698,6 +702,8 @@ interface StackOptions {
supportsVision?: boolean | null;
/** Direct-to-provider vs. aggregator transport (gates transport-sensitive engines like omniglyph). */
providerTransport?: "direct" | "aggregator";
/** Provider resolvido — a contabilidade do omniglyph depende dele. */
provider?: string;
imageTransportFidelity?: ImageTransportFidelity;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;

View File

@@ -16,6 +16,7 @@ import type { RiskGateConfig } from "./riskGate/riskGate.ts";
import type { PipelineCircuitBreakerConfig } from "./pipelineEngineBreaker.ts";
import type { RiskGateStats } from "./riskGate/riskGateStep.ts";
import type { QuantumLockConfig, QuantumLockStats } from "./quantumLock/quantumPatterns.ts";
import type { OmniGlyphAccounting } from "./omniglyphTelemetry.ts";
// Re-export so consumers that already import from this module (e.g. src/lib/db/compression.ts)
// can get ENGINE_IDS without a second bare `@omniroute/open-sse/...engineCatalog.ts` specifier.
@@ -303,6 +304,12 @@ export interface CompressionStats {
validationWarnings?: string[];
validationErrors?: string[];
fallbackApplied?: boolean;
/**
* Contabilidade física do OmniGlyph, normalizada pelo próprio pacote
* (`normalizeAccounting`). Só número e enum — ver `omniglyphTelemetry.ts`
* para a allowlist e o que nunca pode entrar aqui.
*/
omniglyph?: OmniGlyphAccounting;
riskGate?: RiskGateStats;
/**
* Phase 4 (B): which `ultra` tier actually ran for this request.
@@ -341,6 +348,8 @@ export interface CompressionStats {
durationMs?: number;
rejected?: boolean;
rejectReason?: string;
/** Contabilidade física — presente só no passo omniglyph que comprimiu. */
omniglyph?: OmniGlyphAccounting;
}>;
/** Present only when QuantumLock stabilized ≥1 fragment this run. */
quantumLock?: QuantumLockStats;

View File

@@ -88,6 +88,7 @@ export async function applyResponsesWsCompression(
const result = await applyCompressionAsync(adapter.body, mode, {
model: ctx.model,
...resolveOmniGlyphTransport(ctx.provider),
provider: ctx.provider,
config: settings as CompressionConfig,
cachingContext,
});

View File

@@ -168,3 +168,28 @@ test("Claude→OpenAI aguarda o wire alvo e não imageia o corpo fonte", async (
assert.equal(post.compressed, true);
assert.ok(JSON.stringify(post.body).includes('"type":"image_url"'));
});
// O `provider` é threaded explicitamente por cada camada do selector (não é um
// spread cego), então uma camada nova pode derrubá-lo em silêncio: a
// contabilidade cairia para `unknown` e passaria a recusar a semântica de cache
// sem nenhum erro aparecer.
test("provider chega do selector até a contabilidade da engine", async () => {
registerBuiltinCompressionEngines();
const r = await applyCompressionAsync(body(), "stacked", {
model: "claude-fable-5",
provider: "anthropic",
supportsVision: true,
providerTransport: "direct",
imageTransportFidelity: "byte-preserving",
config: { stackedPipeline: [{ engine: "omniglyph" }] } as never,
});
assert.equal(r.compressed, true);
const omniglyphStep = r.stats?.engineBreakdown?.find((e) => e.engine === "omniglyph");
assert.ok(omniglyphStep, "omniglyph step deveria aparecer no engineBreakdown");
assert.equal(
omniglyphStep!.omniglyph?.provider,
"anthropic",
"sem o provider a contabilidade cai para unknown e recusa a semântica de cache"
);
assert.equal(omniglyphStep!.omniglyph?.savings.evidence, "bytes-only");
});

View File

@@ -0,0 +1,121 @@
import { test } from "node:test";
import assert from "node:assert";
import { omniglyphEngine } from "../../../open-sse/services/compression/engines/omniglyphAdapter.ts";
// O OmniGlyph 1.4.0 expõe `normalizeAccounting()`, que classifica o GRAU DE
// EVIDÊNCIA da economia (provider-reported / estimated / bytes-only /
// unavailable) e resolve a semântica de cache por provider — Anthropic usa
// buckets disjuntos, OpenAI/xAI reportam cached como subconjunto do input.
// Somar à mão dá double-count. Antes disso o adapter descartava tudo e a UI
// exibia um número sem dizer de onde ele veio.
const SEGREDO = "sk-ant-api03-SEGREDO-QUE-NAO-PODE-VAZAR-NA-TELEMETRIA";
const DENSE =
"X".repeat(500) +
"\n" +
Array.from(
{ length: 400 },
(_, i) => `const row_${i} = compute(${i * 17}, "${"v".repeat(80)}");`
).join("\n");
function claudeBody(): Record<string, unknown> {
return {
model: "claude-fable-5",
max_tokens: 128,
system: `${DENSE}\nAPI_KEY=${SEGREDO}\ncwd=/home/operador/projeto-secreto`,
messages: [{ role: "user", content: [{ type: "text", text: "oi" }] }],
};
}
const OK = {
model: "claude-fable-5",
provider: "anthropic",
supportsVision: true,
providerTransport: "direct" as const,
imageTransportFidelity: "byte-preserving" as const,
};
test("stats do omniglyph carregam a contabilidade normalizada com grau de evidência", async () => {
const r = await omniglyphEngine.applyAsync!(claudeBody(), OK as never);
assert.equal(r.compressed, true);
const accounting = r.stats?.omniglyph;
assert.ok(accounting, "compressão aplicada deve registrar a contabilidade");
assert.equal(accounting.provider, "anthropic");
assert.ok(
["provider-reported", "estimated", "bytes-only", "unavailable"].includes(
accounting.savings.evidence
),
`evidência inesperada: ${accounting.savings.evidence}`
);
// Sem contadores do provider nesta etapa, a base honesta é o tamanho do corpo.
assert.equal(accounting.savings.evidence, "bytes-only");
assert.ok(typeof accounting.bytes.original === "number" && accounting.bytes.original > 0);
assert.ok(typeof accounting.bytes.transformed === "number");
assert.ok(typeof accounting.images.count === "number" && accounting.images.count > 0);
assert.ok(typeof accounting.images.bytes === "number" && accounting.images.bytes > 0);
});
test("telemetria não carrega conteúdo: nem base64, nem prompt, nem segredo, nem ambiente", async () => {
const r = await omniglyphEngine.applyAsync!(claudeBody(), OK as never);
assert.equal(r.compressed, true);
const serialized = JSON.stringify(r.stats);
// Valores do request.
assert.ok(!serialized.includes(SEGREDO), "segredo do system vazou na telemetria");
assert.ok(!serialized.includes("projeto-secreto"), "caminho do operador vazou");
assert.ok(!serialized.includes("const row_0 = compute"), "texto do system vazou");
assert.ok(!serialized.includes("data:image/png;base64"), "data URL de imagem vazou");
assert.ok(!serialized.includes("iVBORw0KGgo"), "cabeçalho PNG em base64 vazou");
// Chaves do TransformInfo que carregam conteúdo, hash de conteúdo ou ambiente.
for (const proibida of [
"imageSourceText",
"imageSourceTexts",
"recoverable",
"systemSha8",
"claudeMdSha8",
"firstUserSha8",
"unknownStaticTags",
"churningStaticTags",
'"env"',
]) {
assert.ok(!serialized.includes(proibida), `campo proibido na telemetria: ${proibida}`);
}
// A allowlist é positiva: todo valor da contabilidade é número, string de enum
// conhecida, ou objeto desses. Nada de texto livre vindo do request.
const accounting = r.stats?.omniglyph;
assert.ok(accounting);
const enums = new Set([
"anthropic",
"openai",
"xai",
"unknown",
"provider-reported",
"estimated",
"bytes-only",
"unavailable",
"claude-fable-5",
]);
const walk = (value: unknown, path: string): void => {
if (value === undefined || value === null || typeof value === "number") return;
if (typeof value === "string") {
assert.ok(enums.has(value), `string fora da allowlist em ${path}: ${value}`);
return;
}
assert.equal(typeof value, "object", `tipo inesperado em ${path}`);
for (const [k, v] of Object.entries(value as Record<string, unknown>)) walk(v, `${path}.${k}`);
};
walk(accounting, "omniglyph");
});
test("skip não inventa contabilidade", async () => {
const r = await omniglyphEngine.applyAsync!(claudeBody(), {
...OK,
imageTransportFidelity: "unknown",
} as never);
assert.equal(r.compressed, false);
assert.equal(r.stats?.omniglyph, undefined);
});