diff --git a/README.md b/README.md
index 3f14eccce9..da6c91f3bf 100644
--- a/README.md
+++ b/README.md
@@ -877,7 +877,7 @@ Engines run in pipeline order; each is independently toggleable and configurable
9
Aggressive
Summarization + progressive aging of old turns
10
LLMLingua-2
ML semantic pruning via MobileBERT ONNX — code-safe, async
11
Ultra
Heuristic token pruning with an optional small-model (SLM) tier
-
12
OmniGlyph
Experimental context-as-image encoding routed to Claude Fable 5 (most aggressive; opt-in)
+
12
OmniGlyph
Experimental context-as-image encoding for measured Claude Fable 5 on the direct Anthropic wire; GPT 5.6 transformers remain fail-closed pending provider receipts. Four compression profiles (aggressive default, balanced, coding-safe, passthrough) (most aggressive; opt-in)
Code blocks, URLs and structured data are **always preserved** byte-perfect. **One-click presets** combine the engines:
diff --git a/docs/compression/COMPRESSION_ENGINES.md b/docs/compression/COMPRESSION_ENGINES.md
index 8b0268b800..22cffe7c42 100644
--- a/docs/compression/COMPRESSION_ENGINES.md
+++ b/docs/compression/COMPRESSION_ENGINES.md
@@ -19,8 +19,36 @@ OmniRoute compression is built around engine contracts. A mode can run one engin
| `aggressive` | Caveman + history/tool summarizers | Long chat sessions |
| `ultra` | Caveman + pruning helpers | Context-limit recovery |
| `rtk` | RTK | Terminal, shell, build, test, and git output |
+| `omniglyph` | OmniGlyph | Context-as-image on the native provider wire |
| `stacked` | Pipeline, default `rtk -> caveman` | Mixed tool logs and prose, max savings |
+### OmniGlyph compression profiles
+
+The `omniglyph` engine (package `omniglyph`, 1.4.0+) accepts a named semantic profile, set
+globally through `omniglyph.profile` in the compression settings or per step through the
+stacked pipeline's step config:
+
+| Profile | Boundary |
+| -------------- | --------------------------------------------------------------------------- |
+| `aggressive` | Default. The policy the published receipts measured — images system, tool docs and dense history |
+| `balanced` | Keeps live state native, protects the last 8 turns, collapses older closed history |
+| `coding-safe` | Keeps authority, tool schemas and live tool output native, protects the last 12 turns |
+| `passthrough` | Routes without transforming; the engine is skipped |
+
+The profile is a **ceiling, not a floor**: `mergeCompressionProfileOptions` in the package
+refuses to let a caller override reopen a lossy lane the profile closed, so a per-step
+`preserveSystemPrompt: false` cannot re-enable system compression under `coding-safe`.
+
+Measured on this codebase: `coding-safe` and `balanced` raise `minCompressChars` to its
+maximum and keep system, tool schemas and tool results native, so a session that has not
+accumulated history yet stops at `below_min_chars` and the engine transforms nothing. That
+is why the default is `aggressive` rather than the safest profile.
+
+The package resolves its own model scope and profile from its environment configuration.
+OmniRoute never delegates the decision: the adapter pins the model gate to the package's
+most restrictive scope, so host environment settings can only narrow the allowlist, never
+widen it past OmniRoute's measured receipts.
+
## Engine Registry
The registry lives in `open-sse/services/compression/engines/registry.ts`. Engines expose a shared
diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts
index 6188c44352..d54689472c 100644
--- a/open-sse/handlers/chatCore.ts
+++ b/open-sse/handlers/chatCore.ts
@@ -89,6 +89,7 @@ import { checkResourcePressureGuard } from "../utils/resourcePressure.ts";
import { normalizeHeaders } from "../utils/headers.ts";
import { resolveChatCoreRequestFormat } from "./chatCore/requestFormat.ts";
import { resolveChatCoreTargetFormat } from "./chatCore/targetFormat.ts";
+import { resolveOmniGlyphTransport } from "../services/compression/imageTransportPolicy.ts";
import { stripStore, usesClaudeBridge } from "./chatCore/agentRouterProtocol.ts";
import { defaultClaudeToolType } from "./chatCore/claudeToolDefaults.ts";
import { injectSystemPrompt, injectCustomSystemPrompt } from "../services/systemPrompt.ts";
@@ -1611,16 +1612,13 @@ export async function handleChatCore({
// models, which is intentionally NOT `false` so the gate still preserves images.
supportsVision: getResolvedModelCapabilities({ provider, model: effectiveModel })
.supportsVision,
- // Rotas diretas oficiais ('anthropic' API key e 'claude' OAuth) vs agregadores:
- // o engine omniglyph exige 'direct' — agregadores redimensionam imagens
- // (medido 2026-07-06). OAuth 'claude' é rota direta oficial (#7863).
- providerTransport:
- provider === "anthropic" ||
- provider === "claude" ||
- provider === "openai" ||
- provider === "xai"
- ? ("direct" as const)
- : ("aggregator" as const),
+ // 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,
diff --git a/open-sse/services/compression/engines/omniglyphAdapter.ts b/open-sse/services/compression/engines/omniglyphAdapter.ts
index 545c152672..c407b9c9ab 100644
--- a/open-sse/services/compression/engines/omniglyphAdapter.ts
+++ b/open-sse/services/compression/engines/omniglyphAdapter.ts
@@ -9,6 +9,7 @@
* - modelo fora da allowlist medida → skip:model_not_approved
* - providerTransport !== 'direct' → skip:transport_not_direct
* (agregadores redimensionam imagens e destroem a legibilidade — medido)
+ * - imageTransportFidelity !== 'byte-preserving' → skip:transport_fidelity_unknown/resizes
* - wire não é Claude/OpenAI suportado → skip:target_format_not_supported
* - gate de rentabilidade interno do omniglyph decide o resto (patches 28px
* exatos; texto esparso/pequeno passa direto) → skip:not_profitable
@@ -20,14 +21,65 @@ import type { CompressionEngine, CompressionEngineApplyOptions } from "./types.t
import type { CompressionResult } from "../types.ts";
import { createCompressionStats } from "../stats.ts";
import {
- isOmniGlyphSupportedGptModel,
- isOmniGlyphSupportedModel,
+ buildOmniGlyphAccounting,
+ type OmniGlyphAccounting,
+} from "../omniglyphTelemetry.ts";
+import {
+ isOmniGlyphSupportedModelForScope,
+ mergeCompressionProfileOptions,
+ resolveCompressionProfile,
transformAnthropicMessages,
transformOpenAIChatCompletions,
transformOpenAIResponses,
+ type CompressionProfile,
+ 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";
+
+/**
+ * Perfil padrão do OmniRoute.
+ *
+ * `aggressive` é a política que os recibos publicados mediram. `coding-safe` e
+ * `balanced` fixam `minCompressChars` no máximo e só colapsam histórico antigo:
+ * medido nesta base, uma sessão sem histórico acumulado fica em
+ * `below_min_chars` e a engine não faz nada — o operador veria "ligado, 0% de
+ * ganho". Ficam disponíveis como escolha explícita, não como default.
+ */
+const DEFAULT_PROFILE: OmniGlyphSafetyScope = "aggressive";
+
+/** Perfil do passo (mais específico) > perfil global > default do OmniRoute. */
+function resolveProfileName(options?: CompressionEngineApplyOptions): string {
+ const step = options?.stepConfig?.profile;
+ if (typeof step === "string" && step.trim()) return step;
+ const global = (options?.config as { omniglyph?: { profile?: unknown } } | undefined)?.omniglyph
+ ?.profile;
+ if (typeof global === "string" && global.trim()) return global;
+ return DEFAULT_PROFILE;
+}
+
+/**
+ * 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 {
@@ -80,9 +132,36 @@ async function applyOmniglyph(
const model = options?.model ?? (body as { model?: string }).model ?? "";
if (options?.supportsVision !== true) return skip(body, "no_vision");
if (options?.providerTransport !== "direct") return skip(body, "transport_not_direct");
+ // Keep the old direct-call contract usable for standalone callers, but let
+ // production callers override it explicitly. The chat pipeline supplies
+ // `unknown` for every provider without a byte-preservation receipt.
+ if (
+ options?.imageTransportFidelity !== undefined &&
+ options.imageTransportFidelity !== "byte-preserving"
+ ) {
+ return skip(
+ body,
+ options.imageTransportFidelity === "resizes"
+ ? "transport_resizes_images"
+ : "transport_fidelity_unknown"
+ );
+ }
const stage = options?.compressionStage ?? "pre-translation";
const wireFormat = resolveWireFormat(body, options);
+ // A source/target format mismatch means the body is still on the wrong wire,
+ // even when the source itself is native Claude. Defer the engine until the
+ // translated provider body so Claude→OpenAI cannot be imaged once before
+ // translation and then considered again on the target wire.
+ const sourceWireFormat = options?.sourceFormat ?? wireFormat;
+ if (
+ stage === "pre-translation" &&
+ options?.targetFormat &&
+ sourceWireFormat &&
+ options.targetFormat !== sourceWireFormat
+ ) {
+ return skip(body, "requires_post_translation");
+ }
// The pre-translation lane is retained for the existing native Claude
// passthrough. OpenAI requests must wait until translation has produced the
// exact provider wire, otherwise Responses input[] would be flattened by the
@@ -104,62 +183,98 @@ 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");
+ let profile: CompressionProfile;
+ try {
+ profile = resolveCompressionProfile(resolveProfileName(options));
+ } catch {
+ // `resolveCompressionProfile` lança em nome desconhecido. Um perfil que o
+ // pacote não entende não pode virar "roda com a política padrão".
+ return skip(body, "invalid_profile");
+ }
+ if (profile.name === "passthrough") return skip(body, "profile_passthrough");
+ if (!isModelWithinScope(model, profile.name)) {
+ return skip(body, "model_not_approved");
+ }
+ const preserveSystemPrompt =
+ (typeof options?.stepConfig?.preserveSystemPrompt === "boolean"
+ ? options.stepConfig.preserveSystemPrompt
+ : options?.config?.preserveSystemPrompt) === true;
+ // `compressSystem` só existe no transform Anthropic. Os wires OpenAI honram
+ // apenas compressTools/gptHistory/minCompressChars/reflow e sempre trocam a
+ // instrução por um ponteiro para a imagem. Imagear o system quando o OmniRoute
+ // decidiu preservá-lo queimaria o prefixo quente que a política cache-aware
+ // está protegendo — e nada no corpo devolvido denunciaria isso. Sem como
+ // honrar a política nesse wire, a engine pula.
+ if (preserveSystemPrompt && wireFormat !== "claude") {
+ return skip(body, "system_preservation_unsupported_on_wire");
+ }
// 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");
-
const started = Date.now();
let outBody: Record;
+ let accounting: OmniGlyphAccounting | undefined;
try {
- const encoded = new TextEncoder().encode(JSON.stringify(body));
- // Branch explicitly so TS narrows each transformer's return type:
- // the Anthropic wrapper reports `applied`, the OpenAI ones `info.compressed`.
- let applied: boolean;
- let transformed: { body: Uint8Array; info: { compressed: boolean; reason?: string } };
- if (wireFormat === "claude") {
- const result = await transformAnthropicMessages({ body: encoded, model });
- transformed = result;
- applied = result.applied;
- } else {
- const result =
- wireFormat === "openai"
- ? await transformOpenAIChatCompletions(encoded)
- : await transformOpenAIResponses(encoded);
- transformed = result;
- applied = result.info.compressed;
- }
- if (!applied) return skip(body, transformed.info?.reason ?? "not_profitable");
- outBody = JSON.parse(new TextDecoder().decode(transformed.body)) as Record;
+ // The upstream OpenAI transformer resolves its billing/render profile from
+ // body.model. Keep the provider body byte-compatible on output, but use the
+ // already-resolved engine model for that internal gate when a translator
+ // omitted the model or left an alias in place.
+ const transformBody =
+ wireFormat !== "claude" && model && body.model !== model ? { ...body, model } : body;
+ const encoded = new TextEncoder().encode(JSON.stringify(transformBody));
+ const overrides = preserveSystemPrompt ? { compressSystem: false } : {};
+ // Só `transformAnthropicMessages` resolve o perfil por conta própria; os
+ // transformadores OpenAI recebem TransformOptions cru e ignorariam o campo.
+ const openAIOptions = mergeCompressionProfileOptions(profile, overrides);
+ const result =
+ wireFormat === "claude"
+ ? await transformAnthropicMessages({
+ body: encoded,
+ model,
+ options: { ...overrides, profile: profile.name },
+ })
+ : wireFormat === "openai"
+ ? await transformOpenAIChatCompletions(encoded, openAIOptions)
+ : await transformOpenAIResponses(encoded, openAIOptions);
+ const applied = "applied" in result ? result.applied : result.info.compressed;
+ if (!applied) return skip(body, result.info?.reason ?? "not_profitable");
+ outBody = JSON.parse(new TextDecoder().decode(result.body)) as Record;
+ 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 = {
id: "omniglyph",
name: "OmniGlyph",
description:
- "Contexto-como-imagem (Anthropic Fable 5, rota direta): system prompt, tool docs e histórico viram páginas PNG densas — ~10× menos tokens no bloco convertido.",
+ "Contexto-como-imagem para Claude Fable 5 na rota direta medida; wires GPT nativos ficam disponíveis apenas após recibo de fidelidade do provedor.",
icon: "image",
targets: ["messages", "tool_results"],
stackable: true,
@@ -169,7 +284,7 @@ export const omniglyphEngine: CompressionEngine = {
id: "omniglyph",
name: "OmniGlyph",
description:
- "Contexto-como-imagem para Claude Fable 5 e GPT 5.6 via wires nativos Anthropic/OpenAI em rota direta.",
+ "Contexto-como-imagem para Claude Fable 5 na rota direta medida; transformadores GPT nativos permanecem fail-closed até validação do provedor.",
inputScope: "mixed",
targetLatencyMs: 250, // render+encode PNG de páginas grandes
supportsPreview: true,
diff --git a/open-sse/services/compression/engines/types.ts b/open-sse/services/compression/engines/types.ts
index 0a394baccf..a70d6368d0 100644
--- a/open-sse/services/compression/engines/types.ts
+++ b/open-sse/services/compression/engines/types.ts
@@ -7,6 +7,9 @@ export type CompressionWireFormat = "claude" | "openai" | "openai-responses" | s
export type CompressionStage = "pre-translation" | "post-translation";
+/** Whether an upstream route preserves OmniGlyph PNG bytes and dimensions. */
+export type ImageTransportFidelity = "byte-preserving" | "resizes" | "unknown";
+
export interface EngineConfigField {
key: string;
type: "boolean" | "number" | "string" | "select" | "multiselect";
@@ -42,8 +45,11 @@ export interface CompressionEngineApplyOptions {
/** Como o request chega ao provider: rota direta oficial ('direct') vs
* agregador que pode reprocessar imagens ('aggregator'). O engine omniglyph
* exige 'direct' — medição 2026-07-06: agregadores redimensionam as páginas
- * e destroem a legibilidade. undefined = desconhecido = skip (fail-closed). */
+ * e destroem a legibilidade. A política de produção também informa
+ * imageTransportFidelity; chamadas legadas sem esse campo mantêm o gate direct. */
providerTransport?: "direct" | "aggregator";
+ /** Independent image-fidelity gate; direct HTTP does not imply byte preservation. */
+ imageTransportFidelity?: ImageTransportFidelity;
/** Protocol shape before the current compression stage. */
sourceFormat?: CompressionWireFormat;
/** Protocol shape expected by the upstream provider. */
@@ -55,6 +61,10 @@ export interface CompressionEngineApplyOptions {
stepConfig?: Record;
/** 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 {
diff --git a/open-sse/services/compression/imageTransportPolicy.ts b/open-sse/services/compression/imageTransportPolicy.ts
new file mode 100644
index 0000000000..ae1736d1fb
--- /dev/null
+++ b/open-sse/services/compression/imageTransportPolicy.ts
@@ -0,0 +1,33 @@
+/**
+ * Provider-level image transport policy for loss-sensitive compression engines.
+ *
+ * `supportsVision` only says that a model can read images. OmniGlyph also needs
+ * the PNG bytes and dimensions to survive the provider route unchanged. The
+ * allowlist below contains only paths with an existing OmniRoute receipt;
+ * everything else is deliberately classified as unknown and skipped.
+ */
+
+import type { ImageTransportFidelity } from "./engines/types.ts";
+
+export type OmniGlyphTransportPolicy = {
+ providerTransport: "direct" | "aggregator";
+ imageTransportFidelity: ImageTransportFidelity;
+};
+
+const BYTE_PRESERVING_PROVIDERS = new Set(["anthropic", "claude"]);
+
+export function resolveOmniGlyphTransport(
+ provider: string | null | undefined
+): OmniGlyphTransportPolicy {
+ const normalized = typeof provider === "string" ? provider.trim().toLowerCase() : "";
+ if (BYTE_PRESERVING_PROVIDERS.has(normalized)) {
+ return {
+ providerTransport: "direct",
+ imageTransportFidelity: "byte-preserving",
+ };
+ }
+ return {
+ providerTransport: "aggregator",
+ imageTransportFidelity: "unknown",
+ };
+}
diff --git a/open-sse/services/compression/omniglyphTelemetry.ts b/open-sse/services/compression/omniglyphTelemetry.ts
new file mode 100644
index 0000000000..5e7471d126
--- /dev/null
+++ b/open-sse/services/compression/omniglyphTelemetry.ts
@@ -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 }
+ : {}),
+ };
+}
diff --git a/open-sse/services/compression/stackedStepCore.ts b/open-sse/services/compression/stackedStepCore.ts
index ad15d5100d..81bf0c9af0 100644
--- a/open-sse/services/compression/stackedStepCore.ts
+++ b/open-sse/services/compression/stackedStepCore.ts
@@ -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 } : {}),
});
}
diff --git a/open-sse/services/compression/stats.ts b/open-sse/services/compression/stats.ts
index ab6779700f..25c9feed3e 100644
--- a/open-sse/services/compression/stats.ts
+++ b/open-sse/services/compression/stats.ts
@@ -77,7 +77,7 @@ function isOpenAIResponsesPngImagePart(value: unknown): value is OpenAIResponses
}
function pngDimensionsFromDataUrl(value: string): { width: number; height: number } | null {
- const marker = ",base64,";
+ const marker = ";base64,";
const markerIndex = value.indexOf(marker);
if (markerIndex < 0) return null;
return decodePngDimensions(value.slice(markerIndex + marker.length));
diff --git a/open-sse/services/compression/strategySelector.ts b/open-sse/services/compression/strategySelector.ts
index 7fd80a0694..2c9624730a 100644
--- a/open-sse/services/compression/strategySelector.ts
+++ b/open-sse/services/compression/strategySelector.ts
@@ -11,6 +11,7 @@ import type {
CompressionEngineApplyOptions,
CompressionStage,
CompressionWireFormat,
+ ImageTransportFidelity,
} from "./engines/types.ts";
import { applyLiteCompression } from "./lite.ts";
import { cavemanCompress } from "./caveman.ts";
@@ -266,6 +267,7 @@ export function applyCompression(
options?: {
model?: string;
supportsVision?: boolean | null;
+ imageTransportFidelity?: ImageTransportFidelity;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
@@ -292,6 +294,7 @@ function runCompression(
options?: {
model?: string;
supportsVision?: boolean | null;
+ imageTransportFidelity?: ImageTransportFidelity;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
@@ -479,6 +482,9 @@ 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;
compressionStage?: CompressionStage;
@@ -501,6 +507,9 @@ 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;
compressionStage?: CompressionStage;
@@ -693,6 +702,9 @@ 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;
compressionStage?: CompressionStage;
diff --git a/open-sse/services/compression/types.ts b/open-sse/services/compression/types.ts
index 5905a7b49f..f35dfac37f 100644
--- a/open-sse/services/compression/types.ts
+++ b/open-sse/services/compression/types.ts
@@ -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.
@@ -157,6 +158,22 @@ export interface LiveZoneConfig {
enabled: boolean;
}
+/** Perfil semântico do OmniGlyph (pacote 1.4.0+). */
+export type OmniglyphProfile = "coding-safe" | "balanced" | "aggressive" | "passthrough";
+
+/**
+ * Política do OmniGlyph escolhida pelo operador.
+ *
+ * O perfil é um TETO: `mergeCompressionProfileOptions` do pacote não deixa um
+ * override reabrir uma lane que o perfil fechou. Trocar de `aggressive` para
+ * `coding-safe` mantém system, schemas de tools e tool results nativos, ao custo
+ * medido de a engine não fazer nada até a sessão acumular histórico
+ * (`minCompressChars` vai ao máximo). Por isso o default é `aggressive`.
+ */
+export interface OmniglyphConfig {
+ profile: OmniglyphProfile;
+}
+
/** Lite detail settings for proactive request-time transformations. */
export interface LiteConfig {
/** Truncate tool-result strings over 2,000 characters before provider dispatch. */
@@ -206,6 +223,8 @@ export interface CompressionConfig {
comboOverrides: Record;
compressionComboId?: string | null;
stackedPipeline?: CompressionPipelineStep[];
+ /** Política do engine OmniGlyph (perfil semântico). */
+ omniglyph?: OmniglyphConfig;
/** Opt-in QuantumLock cache-prefix stabilization (default off). */
quantumLock?: QuantumLockConfig;
/** Opt-in per-step fidelity gate (default disabled). */
@@ -303,6 +322,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 +366,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;
@@ -460,6 +487,16 @@ export const DEFAULT_COMPRESSION_LANGUAGE_CONFIG: CompressionLanguageConfig = {
enabledPacks: ["en"],
};
+/**
+ * `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 — como o OmniGlyph é opt-in, esse
+ * default entregaria "ligado, 0% de ganho".
+ */
+export const DEFAULT_OMNIGLYPH_CONFIG: OmniglyphConfig = {
+ profile: "aggressive",
+};
+
export const DEFAULT_CONTEXT_EDITING_CONFIG: ContextEditingConfig = {
enabled: false,
};
diff --git a/package-lock.json b/package-lock.json
index 8af4841897..2741e67b37 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.0.2",
+ "omniglyph": "^1.4.0",
"onnxruntime-node": "~1.24.3",
"open": "^11.0.1",
"ora": "^9.4.1",
@@ -29929,9 +29929,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 d6ff6ad849..bc810f2f84 100644
--- a/package.json
+++ b/package.json
@@ -305,7 +305,7 @@
"next-intl": "^4.13.6",
"next-themes": "^0.4.6",
"node-machine-id": "^1.1.12",
- "omniglyph": "^1.0.2",
+ "omniglyph": "^1.4.0",
"open": "^11.0.1",
"ora": "^9.4.1",
"parse5": "^8.0.1",
diff --git a/src/app/(dashboard)/dashboard/context/omniglyph/OmniglyphContextPageClient.tsx b/src/app/(dashboard)/dashboard/context/omniglyph/OmniglyphContextPageClient.tsx
index 016888454d..5a14d801e3 100644
--- a/src/app/(dashboard)/dashboard/context/omniglyph/OmniglyphContextPageClient.tsx
+++ b/src/app/(dashboard)/dashboard/context/omniglyph/OmniglyphContextPageClient.tsx
@@ -17,10 +17,22 @@ import { SAMPLE_BEFORE_TEXT, SAMPLE_PAGE_PNG_DATA_URI, SAMPLE_METRICS } from "./
interface CompressionConfigLite {
engines?: Record;
+ omniglyph?: { profile?: string };
}
type EngineMap = Record;
+/** Perfis do pacote, na ordem do mais permissivo ao mais restrito. O primeiro é
+ * o default: a política que os recibos publicados mediram. */
+const PROFILES = [
+ { id: "aggressive", key: "aggressive" },
+ { id: "balanced", key: "balanced" },
+ { id: "coding-safe", key: "codingSafe" },
+ { id: "passthrough", key: "passthrough" },
+] as const;
+
+type ProfileId = (typeof PROFILES)[number]["id"];
+
/** The measured fail-closed gate chain, in evaluation order. Every no-op is telemetered
* as `skip:`; the engine only fires when all pass. */
const GATES = [
@@ -157,6 +169,40 @@ function GatesCard() {
);
}
+function ProfileCard(props: {
+ profile: ProfileId;
+ disabled: boolean;
+ onChange: (next: ProfileId) => void;
+}) {
+ const t = useTranslations("omniglyph");
+ const selected = PROFILES.find((p) => p.id === props.profile) ?? PROFILES[0];
+ return (
+
+