mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-19 05:32:19 +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>
118 lines
5.2 KiB
TypeScript
118 lines
5.2 KiB
TypeScript
/**
|
|
* Core telemetry + step-decision machinery for the stacked compression pipeline, extracted
|
|
* from `strategySelector.ts` so that god-file stays bounded. This module is a leaf: it depends
|
|
* only on the shared compression types, so it never participates in a cycle.
|
|
*
|
|
* It holds the per-run accumulator (`StackAccumulator` + `createStackAccumulator`), the TV1
|
|
* bail-out config + advance decision (`BailoutConfig` + `decideStep`), and the per-step
|
|
* telemetry fold (`mergeStackStep`). The sync/async stacked loops in `strategySelector.ts`
|
|
* consume these.
|
|
*/
|
|
|
|
import type { CompressionResult, CompressionStats } from "./types.ts";
|
|
|
|
/**
|
|
* TV1 — Opt-in bail-out configuration for the stacked pipeline.
|
|
* When enabled: a step that throws is silently skipped (verbatim kept); a step whose gain is
|
|
* below `minGainPercent` is also skipped. DEFAULT = disabled (byte-identical to pre-TV1).
|
|
*/
|
|
export interface BailoutConfig {
|
|
enabled: boolean;
|
|
/** Minimum savings percent required to advance currentBody. Default: 10. */
|
|
minGainPercent?: number;
|
|
}
|
|
|
|
/** Accumulates per-step telemetry across a stacked run (shared sync/async). */
|
|
export interface StackAccumulator {
|
|
techniques: Set<string>;
|
|
rules: Set<string>;
|
|
breakdown: NonNullable<CompressionStats["engineBreakdown"]>;
|
|
rtkRawOutputPointers: NonNullable<CompressionStats["rtkRawOutputPointers"]>;
|
|
validationWarnings: Set<string>;
|
|
validationErrors: Set<string>;
|
|
fallbackApplied: boolean;
|
|
}
|
|
|
|
export function createStackAccumulator(): StackAccumulator {
|
|
return {
|
|
techniques: new Set<string>(),
|
|
rules: new Set<string>(),
|
|
breakdown: [],
|
|
rtkRawOutputPointers: [],
|
|
validationWarnings: new Set<string>(),
|
|
validationErrors: new Set<string>(),
|
|
fallbackApplied: false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* TV1 — Pure helper that decides whether a completed step should advance `currentBody`. Called
|
|
* only when bail-out is ENABLED; the loops bypass it on the default-off path (zero cost). Returns
|
|
* `{ advance: true }` to accept the step, or `{ advance: false }` to skip it (verbatim kept).
|
|
*/
|
|
export function decideStep(
|
|
result: CompressionResult,
|
|
bailout: BailoutConfig
|
|
): { advance: boolean } {
|
|
if (!result.compressed) return { advance: false };
|
|
// Clamp: a negative minGainPercent would mean "always advance" (invalid state).
|
|
const minGain = Math.max(0, bailout.minGainPercent ?? 10);
|
|
const gain = result.stats?.savingsPercent ?? 0;
|
|
if (gain < minGain) return { advance: false };
|
|
return { advance: true };
|
|
}
|
|
|
|
/**
|
|
* A dispatched step whose engine found nothing eligible (e.g. session-dedup with no repeated
|
|
* blocks, ccr below its min-chars threshold) returns `stats: null` instead of throwing or
|
|
* advancing. Left unrecorded, that step vanishes from the pipeline's telemetry with zero trace —
|
|
* no `engineBreakdown` entry, no warning, no error (#6479, #6491). Surface it as a validation
|
|
* warning so operators can tell "engine ran but had nothing to do" apart from "engine never ran".
|
|
*/
|
|
function recordNullStatsStep(acc: StackAccumulator, engineId: string): void {
|
|
acc.validationWarnings.add(`${engineId}: skipped (no eligible content)`);
|
|
}
|
|
|
|
/** Folds one engine result into the accumulator (telemetry + breakdown entry). */
|
|
export function mergeStackStep(
|
|
acc: StackAccumulator,
|
|
engineId: string,
|
|
result: CompressionResult
|
|
): void {
|
|
if (!result.stats) {
|
|
// No-op engine (e.g. ccr / session-dedup found no candidate): stats is null so there is no
|
|
// telemetry to fold, but the engine still RAN — record a zero-savings breakdown entry so its
|
|
// identity survives. Without this the breakdown stays empty and ensureEngineBreakdown
|
|
// synthesizes a generic "stacked" 0% node, hiding which engine an operator actually asked for.
|
|
// Also surface a validation warning so operators can tell "engine ran but had nothing to do"
|
|
// apart from "engine never ran" (#6479, #6491).
|
|
recordNullStatsStep(acc, engineId);
|
|
acc.breakdown.push({
|
|
engine: engineId,
|
|
originalTokens: 0,
|
|
compressedTokens: 0,
|
|
savingsPercent: 0,
|
|
techniquesUsed: [],
|
|
});
|
|
return;
|
|
}
|
|
result.stats.techniquesUsed.forEach((technique) => acc.techniques.add(technique));
|
|
result.stats.rulesApplied?.forEach((rule) => acc.rules.add(rule));
|
|
result.stats.rtkRawOutputPointers?.forEach((pointer) => acc.rtkRawOutputPointers.push(pointer));
|
|
result.stats.validationWarnings?.forEach((warning) => acc.validationWarnings.add(warning));
|
|
result.stats.validationErrors?.forEach((error) => acc.validationErrors.add(error));
|
|
acc.fallbackApplied = acc.fallbackApplied || result.stats.fallbackApplied === true;
|
|
acc.breakdown.push({
|
|
engine: engineId,
|
|
originalTokens: result.stats.originalTokens,
|
|
compressedTokens: result.stats.compressedTokens,
|
|
savingsPercent: result.stats.savingsPercent,
|
|
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 } : {}),
|
|
});
|
|
}
|