mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 17:12:27 +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>
157 lines
5.8 KiB
TypeScript
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 }
|
|
: {}),
|
|
};
|
|
}
|