From 6b05eedc8da091b809326cd5d6a75b4656e5816b Mon Sep 17 00:00:00 2001 From: Xiangzhe Date: Mon, 17 Aug 2026 23:56:01 -0300 Subject: [PATCH] feat(compression): target-wire OmniGlyph stage and transport fidelity gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 2 +- open-sse/handlers/chatCore.ts | 14 +-- .../compression/engines/omniglyphAdapter.ts | 61 ++++++++-- .../services/compression/engines/types.ts | 8 +- .../compression/imageTransportPolicy.ts | 33 ++++++ open-sse/services/compression/stats.ts | 2 +- .../services/compression/strategySelector.ts | 6 + package-lock.json | 2 +- package.json | 2 +- .../codex-responses-ws/compression.ts | 15 ++- src/i18n/messages/en.json | 18 +-- src/i18n/messages/pt-BR.json | 18 +-- .../compression/image-aware-tokens.test.ts | 32 ++++- .../compression/omniglyph-adapter.test.ts | 42 ++++++- .../omniglyph-chatcore-plumbing.test.ts | 25 +++- .../unit/compression/omniglyph-import.test.ts | 1 + .../compression/omniglyph-plumbing.test.ts | 109 ++++++++++++++++++ .../compression/omniglyph-single-mode.test.ts | 10 +- 18 files changed, 342 insertions(+), 58 deletions(-) create mode 100644 open-sse/services/compression/imageTransportPolicy.ts diff --git a/README.md b/README.md index a5ef67269e..9036898e85 100644 --- a/README.md +++ b/README.md @@ -853,7 +853,7 @@ Engines run in pipeline order; each is independently toggleable and configurable 9AggressiveSummarization + progressive aging of old turns 10LLMLingua-2ML semantic pruning via MobileBERT ONNX — code-safe, async 11UltraHeuristic token pruning with an optional small-model (SLM) tier - 12OmniGlyphExperimental context-as-image encoding routed to Claude Fable 5 (most aggressive; opt-in) + 12OmniGlyphExperimental context-as-image encoding for measured Claude Fable 5 on the direct Anthropic wire; GPT 5.6 transformers remain fail-closed pending provider receipts (most aggressive; opt-in) Code blocks, URLs and structured data are **always preserved** byte-perfect. **One-click presets** combine the engines: diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 0235589a5b..be925b60bf 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -88,6 +88,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"; @@ -1548,16 +1549,9 @@ 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), 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 3f62d5febf..b076234972 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 @@ -25,6 +26,7 @@ import { transformAnthropicMessages, transformOpenAIChatCompletions, transformOpenAIResponses, + transformRequest, } from "omniglyph"; import { isModelImageable } from "omniglyph/applicability"; @@ -80,9 +82,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 @@ -112,20 +141,32 @@ async function applyOmniglyph( // 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; try { - const encoded = new TextEncoder().encode(JSON.stringify(body)); + // 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 preserveSystemPrompt = + (typeof options?.stepConfig?.preserveSystemPrompt === "boolean" + ? options.stepConfig.preserveSystemPrompt + : options?.config?.preserveSystemPrompt) === true; const result = - wireFormat === "claude" - ? await transformAnthropicMessages({ body: encoded, model }) - : wireFormat === "openai" - ? await transformOpenAIChatCompletions(encoded) - : await transformOpenAIResponses(encoded); - const applied = wireFormat === "claude" ? result.applied : result.info.compressed; + wireFormat === "claude" && preserveSystemPrompt + ? await transformRequest(encoded, { compressSystem: false }) + : wireFormat === "claude" + ? await transformAnthropicMessages({ body: encoded, model }) + : wireFormat === "openai" + ? await transformOpenAIChatCompletions(encoded) + : await transformOpenAIResponses(encoded); + 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; } 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. @@ -150,7 +191,7 @@ 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, @@ -160,7 +201,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..0caaae9346 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. */ 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/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 76a67d5fcb..0200d66f88 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,7 @@ export async function applyCompressionAsync( supportsVision?: boolean | null; /** Direct-to-provider vs. aggregator transport (gates transport-sensitive engines like omniglyph). */ providerTransport?: "direct" | "aggregator"; + imageTransportFidelity?: ImageTransportFidelity; sourceFormat?: CompressionWireFormat; targetFormat?: CompressionWireFormat; compressionStage?: CompressionStage; @@ -501,6 +505,7 @@ async function runCompressionAsync( supportsVision?: boolean | null; /** Direct-to-provider vs. aggregator transport (gates transport-sensitive engines like omniglyph). */ providerTransport?: "direct" | "aggregator"; + imageTransportFidelity?: ImageTransportFidelity; sourceFormat?: CompressionWireFormat; targetFormat?: CompressionWireFormat; compressionStage?: CompressionStage; @@ -693,6 +698,7 @@ interface StackOptions { supportsVision?: boolean | null; /** Direct-to-provider vs. aggregator transport (gates transport-sensitive engines like omniglyph). */ providerTransport?: "direct" | "aggregator"; + imageTransportFidelity?: ImageTransportFidelity; sourceFormat?: CompressionWireFormat; targetFormat?: CompressionWireFormat; compressionStage?: CompressionStage; diff --git a/package-lock.json b/package-lock.json index 4769372f63..71fd7e0093 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.3.1", "onnxruntime-node": "~1.24.3", "open": "^11.0.0", "ora": "^9.4.1", diff --git a/package.json b/package.json index a4998e690f..94a6ec3705 100644 --- a/package.json +++ b/package.json @@ -304,7 +304,7 @@ "next-intl": "^4.13.6", "next-themes": "^0.4.6", "node-machine-id": "^1.1.12", - "omniglyph": "^1.0.2", + "omniglyph": "^1.3.1", "open": "^11.0.0", "ora": "^9.4.1", "parse5": "^8.0.1", diff --git a/src/app/api/internal/codex-responses-ws/compression.ts b/src/app/api/internal/codex-responses-ws/compression.ts index f4cd889c13..72ca922fb4 100644 --- a/src/app/api/internal/codex-responses-ws/compression.ts +++ b/src/app/api/internal/codex-responses-ws/compression.ts @@ -20,6 +20,7 @@ import { logger } from "@omniroute/open-sse/utils/logger.ts"; import { estimateTokens } from "@omniroute/open-sse/services/contextManager.ts"; import { adaptBodyForCompression } from "@omniroute/open-sse/services/compression/bodyAdapter.ts"; +import { resolveOmniGlyphTransport } from "@omniroute/open-sse/services/compression/imageTransportPolicy.ts"; import type { CompressionConfig, CompressionResult, @@ -55,13 +56,16 @@ export async function applyResponsesWsCompression( if (!enabled || !settings) return responseBody; const adapter = adaptBodyForCompression(responseBody); - if (!adapter.adapted || !Array.isArray(adapter.body.messages) || adapter.body.messages.length === 0) { + if ( + !adapter.adapted || + !Array.isArray(adapter.body.messages) || + adapter.body.messages.length === 0 + ) { return responseBody; } - const { selectCompressionStrategy, applyCompressionAsync } = await import( - "@omniroute/open-sse/services/compression/strategySelector.ts" - ); + const { selectCompressionStrategy, applyCompressionAsync } = + await import("@omniroute/open-sse/services/compression/strategySelector.ts"); const estimatedTokens = estimateTokens(adapter.body.messages); const cachingContext = { @@ -83,8 +87,7 @@ export async function applyResponsesWsCompression( const result = await applyCompressionAsync(adapter.body, mode, { model: ctx.model, - providerTransport: - ctx.provider === "anthropic" || ctx.provider === "claude" ? "direct" : "aggregator", + ...resolveOmniGlyphTransport(ctx.provider), config: settings as CompressionConfig, cachingContext, }); diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 90291a11d9..6a6c37da90 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -8534,13 +8534,13 @@ }, "omniglyph": { "preview": "Preview", - "description": "Context-as-image compression. Renders the system prompt, tool documentation, and dense history as compact PNG pages that Claude Fable 5 reads instead of text. Image tokens are billed by dimensions rather than characters, so the converted block costs about 10× less. Direct Anthropic route only.", + "description": "Context-as-image compression. Renders the system prompt, tool documentation, and dense history as compact PNG pages for the measured Claude Fable 5 path. The published OmniGlyph package also exposes native GPT 5.6 transformers, but OmniRoute keeps those routes fail-closed until provider fidelity and reading receipts exist.", "economicsTitle": "The economics", "economics": { "fewerTokens": "fewer tokens on the converted block", "savings": "end-to-end savings (measured)", "imageTokens": "image tokens for a 1568×728 page (about 28k characters)", - "accuracy": "reading accuracy on Fable 5 (n=30)" + "accuracy": "scoped Fable 5 reading receipt" }, "beforeAfterTitle": "Before → after", "blockSavings": "−{percent}% tokens on this block", @@ -8553,18 +8553,18 @@ "gates": { "model": { "label": "Model", - "pass": "claude-fable-5", - "why": "Only Fable 5 reads dense pages at 100% accuracy (measured, n=30). GPT-5.5 and Gemini 2.5 Flash are blocked." + "pass": "claude-fable-5 (measured); gpt-5.6 (HOLD)", + "why": "Only the measured Fable 5 path is enabled in OmniRoute. GPT 5.6 is structurally supported by the package but remains blocked until its provider receipt; unverified families such as Grok stay text-only." }, "transport": { "label": "Transport", - "pass": "direct Anthropic", - "why": "Aggregators resample images and destroy legibility — only the direct route is authoritative." + "pass": "direct provider", + "why": "Aggregators may resample images and destroy legibility — only a direct provider route is authoritative." }, "format": { "label": "Format", - "pass": "native Claude", - "why": "The body must use Claude format and must never put a system role inside messages." + "pass": "native Claude/OpenAI", + "why": "The body must match the provider wire: Claude Messages, OpenAI Chat Completions, or native OpenAI Responses input[]." }, "profitable": { "label": "Profitable", @@ -8573,7 +8573,7 @@ } }, "enableTitle": "Enable the engine", - "enableDescription": "Runs last in the stack (after RTK/Caveman cleans the text, OmniGlyph converts the remainder to images) and also runs standalone through omniglyph mode. This is a preview and remains off by default until end-to-end validation is complete.", + "enableDescription": "Runs last in the stack after text engines clean the request, using the native provider wire; OpenAI Chat/Responses requests are compressed after translation so their shape is preserved. It also runs standalone through omniglyph mode. This is a preview and remains off by default until end-to-end validation is complete.", "saved": "Saved.", "saveFailed": "Could not save.", "enableAria": "Enable the OmniGlyph engine", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 54f9d8c27a..cfa6756214 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -8534,13 +8534,13 @@ }, "omniglyph": { "preview": "Visualização", - "description": "Compressão de contexto como imagem. Renderiza o prompt de sistema, a documentação de ferramentas e o histórico denso como páginas PNG compactas que o Claude Fable 5 lê em vez de texto. Os tokens de imagem são cobrados por dimensões em vez de caracteres, então o bloco convertido custa cerca de 10× menos. Somente na rota direta da Anthropic.", + "description": "Compressão de contexto como imagem. Renderiza o prompt de sistema, a documentação de ferramentas e o histórico denso como páginas PNG compactas para a rota medida do Claude Fable 5. O pacote OmniGlyph também expõe transformadores nativos para GPT 5.6, mas o OmniRoute mantém essas rotas fail-closed até existirem recibos de fidelidade do provedor e de leitura.", "economicsTitle": "A economia", "economics": { "fewerTokens": "menos tokens no bloco convertido", "savings": "economia de ponta a ponta (medida)", "imageTokens": "tokens de imagem para uma página de 1568×728 (cerca de 28 mil caracteres)", - "accuracy": "precisão de leitura no Fable 5 (n=30)" + "accuracy": "recibo de leitura do Fable 5 (escopo medido)" }, "beforeAfterTitle": "Antes → depois", "blockSavings": "−{percent}% de tokens neste bloco", @@ -8553,18 +8553,18 @@ "gates": { "model": { "label": "Modelo", - "pass": "claude-fable-5", - "why": "Somente o Fable 5 lê páginas densas com 100% de precisão (medido, n=30). GPT-5.5 e Gemini 2.5 Flash são bloqueados." + "pass": "claude-fable-5 (medido); gpt-5.6 (HOLD)", + "why": "Somente a rota medida do Fable 5 está habilitada no OmniRoute. O GPT 5.6 tem suporte estrutural no pacote, mas continua bloqueado até seu recibo do provedor; famílias não verificadas, como Grok, permanecem em texto." }, "transport": { "label": "Transporte", - "pass": "direto Anthropic", - "why": "Agregadores reamostram imagens e destroem a legibilidade — apenas a rota direta é confiável." + "pass": "provedor direto", + "why": "Agregadores podem reamostrar imagens e destruir a legibilidade — apenas uma rota direta do provedor é confiável." }, "format": { "label": "Formato", - "pass": "Claude nativo", - "why": "O corpo deve usar o formato Claude e nunca colocar um papel system dentro de messages." + "pass": "Claude/OpenAI nativo", + "why": "O corpo precisa corresponder ao wire do provedor: Claude Messages, OpenAI Chat Completions ou input[] nativo do OpenAI Responses." }, "profitable": { "label": "Rentável", @@ -8573,7 +8573,7 @@ } }, "enableTitle": "Ativar a engine", - "enableDescription": "Executa por último na pilha (depois que RTK/Caveman limpa o texto, o OmniGlyph converte o restante em imagens) e também roda de forma independente pelo modo omniglyph. Este é um preview e permanece desativado por padrão até que a validação de ponta a ponta seja concluída.", + "enableDescription": "Executa por último na pilha, depois que os engines de texto limpam a solicitação, usando o wire nativo do provedor; solicitações OpenAI Chat/Responses são comprimidas depois da tradução para preservar seu formato. Também roda de forma independente pelo modo omniglyph. Este é um preview e permanece desativado por padrão até que a validação de ponta a ponta seja concluída.", "saved": "Salvo.", "saveFailed": "Não foi possível salvar.", "enableAria": "Ativar a engine OmniGlyph", diff --git a/tests/unit/compression/image-aware-tokens.test.ts b/tests/unit/compression/image-aware-tokens.test.ts index 8a0e224dd4..32c18336c1 100644 --- a/tests/unit/compression/image-aware-tokens.test.ts +++ b/tests/unit/compression/image-aware-tokens.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert"; import { estimateCompressionTokens } from "../../../open-sse/services/compression/stats.ts"; -import { transformAnthropicMessages } from "omniglyph"; +import { transformAnthropicMessages, transformOpenAIChatCompletions } from "omniglyph"; const CHARS_PER_TOKEN = 4; @@ -67,3 +67,33 @@ test("regressão: corpo sem imagem estima o MESMO valor de antes (char-count pur const expected = Math.ceil(JSON.stringify(plainBody).length / CHARS_PER_TOKEN); assert.equal(estimateCompressionTokens(plainBody), expected); }); + +test("estimateCompressionTokens contabiliza image_url PNG no wire OpenAI", async () => { + const originalBody = { + model: "gpt-5.6", + messages: [ + { role: "system", content: DENSE }, + { role: "user", content: "oi" }, + ], + }; + const encoded = new TextEncoder().encode(JSON.stringify(originalBody)); + const result = await transformOpenAIChatCompletions(encoded); + assert.equal(result.info.compressed, true); + const compressedBody = JSON.parse(new TextDecoder().decode(result.body)) as Record< + string, + unknown + >; + assert.ok(JSON.stringify(compressedBody).includes('"type":"image_url"')); + + const originalEstimate = estimateCompressionTokens(originalBody); + const compressedEstimate = estimateCompressionTokens(compressedBody); + const naiveEstimate = Math.ceil(JSON.stringify(compressedBody).length / CHARS_PER_TOKEN); + assert.ok( + compressedEstimate < naiveEstimate, + `expected image-aware (${compressedEstimate}) < base64 char estimate (${naiveEstimate})` + ); + assert.ok( + compressedEstimate < originalEstimate, + `expected OpenAI image compression (${compressedEstimate}) < original (${originalEstimate})` + ); +}); diff --git a/tests/unit/compression/omniglyph-adapter.test.ts b/tests/unit/compression/omniglyph-adapter.test.ts index c003846dfb..c463df2603 100644 --- a/tests/unit/compression/omniglyph-adapter.test.ts +++ b/tests/unit/compression/omniglyph-adapter.test.ts @@ -18,11 +18,17 @@ function claudeBody(): Record { messages: [{ role: "user", content: [{ type: "text", text: "oi" }] }], }; } -const OK = { model: "claude-fable-5", supportsVision: true, providerTransport: "direct" as const }; +const OK = { + model: "claude-fable-5", + supportsVision: true, + providerTransport: "direct" as const, + imageTransportFidelity: "byte-preserving" as const, +}; const GPT_OK = { model: "gpt-5.6", supportsVision: true, providerTransport: "direct" as const, + imageTransportFidelity: "byte-preserving" as const, sourceFormat: "openai" as const, targetFormat: "openai" as const, compressionStage: "post-translation" as const, @@ -82,6 +88,15 @@ test("skip fail-closed: sem supportsVision / transporte agregador / undefined", } }); +test("skip fail-closed: rota direta sem recibo de fidelidade de imagem", async () => { + const r = await omniglyphEngine.applyAsync!(claudeBody(), { + ...OK, + imageTransportFidelity: "unknown", + }); + assert.equal(r.compressed, false); + assert.ok(r.stats?.techniquesUsed.includes("skip:transport_fidelity_unknown")); +}); + test("skip: modelo fora da allowlist medida", async () => { const body = { ...claudeBody(), model: "gpt-5.5" }; const r = await omniglyphEngine.applyAsync!(body, { ...OK, model: "gpt-5.5" }); @@ -110,6 +125,31 @@ test("cache_control do cliente sobrevive byte a byte", async () => { assert.ok(JSON.stringify(r.body).includes('"cache_control"')); }); +test("preserveSystemPrompt mantém o sistema nativo e ainda pode comprimir tool_result", async () => { + const body = { + ...claudeBody(), + messages: [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call_1", + content: DENSE, + }, + ], + }, + ], + }; + const r = await omniglyphEngine.applyAsync!(body, { + ...OK, + config: { preserveSystemPrompt: true } as never, + }); + assert.equal(r.compressed, true); + assert.equal((r.body as { system?: unknown }).system, DENSE); + assert.ok(JSON.stringify(r.body).includes('"type":"image"')); +}); + test("apply síncrono é pass-through seguro (engine async-only)", () => { const body = claudeBody(); const r = omniglyphEngine.apply(body, OK); diff --git a/tests/unit/compression/omniglyph-chatcore-plumbing.test.ts b/tests/unit/compression/omniglyph-chatcore-plumbing.test.ts index a21a3af9ff..8dc8221da0 100644 --- a/tests/unit/compression/omniglyph-chatcore-plumbing.test.ts +++ b/tests/unit/compression/omniglyph-chatcore-plumbing.test.ts @@ -1,12 +1,27 @@ import { readFileSync } from "node:fs"; import assert from "node:assert/strict"; import { test } from "node:test"; +import { resolveOmniGlyphTransport } from "../../../open-sse/services/compression/imageTransportPolicy.ts"; -test("chatCore treats both Anthropic providers as direct OmniGlyph transports", () => { +test("chatCore uses the measured Anthropic byte-preserving transport policy", () => { const chatCore = readFileSync("open-sse/handlers/chatCore.ts", "utf8"); - assert.match( - chatCore, - /providerTransport:\s*provider === "anthropic" \|\| provider === "claude"[\s\S]{0,80}?"direct"/ - ); + assert.match(chatCore, /resolveOmniGlyphTransport\(provider\)/); + const translationIndex = chatCore.indexOf("translatedBody = translateRequest("); + const postCompressionIndex = chatCore.indexOf("if (runPostTranslationCompression"); + assert.ok(translationIndex >= 0, "chatCore deve manter a tradução explícita"); + assert.ok(postCompressionIndex > translationIndex, "OmniGlyph target-wire roda após tradução"); + assert.match(chatCore, /compressionStage: "post-translation"/); + assert.deepEqual(resolveOmniGlyphTransport("anthropic"), { + providerTransport: "direct", + imageTransportFidelity: "byte-preserving", + }); + assert.deepEqual(resolveOmniGlyphTransport("claude"), { + providerTransport: "direct", + imageTransportFidelity: "byte-preserving", + }); + assert.deepEqual(resolveOmniGlyphTransport("openai"), { + providerTransport: "aggregator", + imageTransportFidelity: "unknown", + }); }); diff --git a/tests/unit/compression/omniglyph-import.test.ts b/tests/unit/compression/omniglyph-import.test.ts index 35d3f0a756..170d6bf105 100644 --- a/tests/unit/compression/omniglyph-import.test.ts +++ b/tests/unit/compression/omniglyph-import.test.ts @@ -6,6 +6,7 @@ test("pacote omniglyph exporta a API que o adapter consome", async () => { assert.equal(typeof mod.transformAnthropicMessages, "function"); assert.equal(typeof mod.transformOpenAIChatCompletions, "function"); assert.equal(typeof mod.transformOpenAIResponses, "function"); + assert.equal(typeof mod.transformRequest, "function"); assert.equal(typeof mod.isOmniGlyphSupportedModel, "function"); assert.equal(typeof mod.isOmniGlyphSupportedGptModel, "function"); assert.equal(mod.isOmniGlyphSupportedModel("claude-fable-5"), true); diff --git a/tests/unit/compression/omniglyph-plumbing.test.ts b/tests/unit/compression/omniglyph-plumbing.test.ts index e5231a10a7..5ef741a020 100644 --- a/tests/unit/compression/omniglyph-plumbing.test.ts +++ b/tests/unit/compression/omniglyph-plumbing.test.ts @@ -18,6 +18,20 @@ const body = () => ({ messages: [{ role: "user", content: [{ type: "text", text: "oi" }] }], }); +const openaiBody = () => ({ + model: "gpt-5.6", + messages: [ + { role: "system", content: DENSE }, + { role: "user", content: "oi" }, + ], +}); + +const responsesBody = () => ({ + model: "gpt-5.6", + instructions: DENSE, + input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "oi" }] }], +}); + // Agora que `estimateCompressionTokens` (stats.ts) é image-aware (Task 6), o guard // honesto de inflação agregada do stacked (`guardPipelineInflation` em // pipelineGuards.ts) não reverte mais a saída imageada do omniglyph: o estimador @@ -30,6 +44,7 @@ test("stacked com step omniglyph recebe providerTransport (engine roda, não é model: "claude-fable-5", supportsVision: true, providerTransport: "direct", + imageTransportFidelity: "byte-preserving", config: { stackedPipeline: [{ engine: "rtk" }, { engine: "omniglyph" }] } as never, }); const omniglyphStep = r.stats?.engineBreakdown?.find((e) => e.engine === "omniglyph"); @@ -59,3 +74,97 @@ test("stacked sem providerTransport 'direct' pula omniglyph (transport_not_direc assert.ok(omniglyphStep, "omniglyph step deveria aparecer no engineBreakdown mesmo pulado"); assert.ok(omniglyphStep!.techniquesUsed.includes("skip:transport_not_direct")); }); + +test("stacked pós-tradução preserva o wire OpenAI Chat e executa OmniGlyph nativo", async () => { + registerBuiltinCompressionEngines(); + const r = await applyCompressionAsync(openaiBody(), "stacked", { + model: "gpt-5.6", + supportsVision: true, + providerTransport: "direct", + imageTransportFidelity: "byte-preserving", + sourceFormat: "openai", + targetFormat: "openai", + compressionStage: "post-translation", + config: { stackedPipeline: [{ engine: "rtk" }, { engine: "omniglyph" }] } as never, + }); + const omniglyphStep = r.stats?.engineBreakdown?.find((e) => e.engine === "omniglyph"); + assert.ok(omniglyphStep); + assert.ok(omniglyphStep!.techniquesUsed.includes("omniglyph:context-as-image")); + assert.equal(r.compressed, true); + assert.ok(JSON.stringify(r.body).includes('"type":"image_url"')); + assert.ok( + r.stats?.validationWarnings?.some((warning) => warning.includes("rtk: skipped (stage")), + "engines sem suporte ao estágio devem ser explicitamente pulados" + ); +}); + +test("stacked pós-tradução não adapta input[] Responses para messages[] antes do OmniGlyph", async () => { + registerBuiltinCompressionEngines(); + const r = await applyCompressionAsync(responsesBody(), "stacked", { + model: "gpt-5.6", + supportsVision: true, + providerTransport: "direct", + imageTransportFidelity: "byte-preserving", + sourceFormat: "openai-responses", + targetFormat: "openai-responses", + compressionStage: "post-translation", + config: { stackedPipeline: [{ engine: "omniglyph" }] } as never, + }); + assert.equal(r.compressed, true); + assert.ok(JSON.stringify(r.body).includes('"type":"input_image"')); + assert.ok(Array.isArray((r.body as { input?: unknown }).input)); +}); + +test("OpenAI→Claude usa o wire traduzido sem uma segunda tradução", async () => { + registerBuiltinCompressionEngines(); + const translatedClaudeBody = body(); + const r = await applyCompressionAsync(translatedClaudeBody, "omniglyph", { + model: "claude-fable-5", + supportsVision: true, + providerTransport: "direct", + imageTransportFidelity: "byte-preserving", + sourceFormat: "openai", + targetFormat: "claude", + compressionStage: "post-translation", + }); + assert.equal(r.compressed, true); + assert.ok(JSON.stringify(r.body).includes('"type":"image"')); + assert.ok(!JSON.stringify(r.body).includes('"type":"image_url"')); +}); + +test("Claude→OpenAI aguarda o wire alvo e não imageia o corpo fonte", async () => { + registerBuiltinCompressionEngines(); + const pre = await applyCompressionAsync(body(), "omniglyph", { + model: "claude-fable-5", + supportsVision: true, + providerTransport: "direct", + imageTransportFidelity: "byte-preserving", + sourceFormat: "claude", + targetFormat: "openai", + compressionStage: "pre-translation", + }); + assert.equal(pre.compressed, false); + assert.ok(pre.stats?.techniquesUsed.includes("skip:requires_post_translation")); + + const post = await applyCompressionAsync( + { + model: "gpt-5.6", + messages: [ + { role: "system", content: DENSE }, + { role: "user", content: "oi" }, + ], + }, + "omniglyph", + { + model: "gpt-5.6", + supportsVision: true, + providerTransport: "direct", + imageTransportFidelity: "byte-preserving", + sourceFormat: "claude", + targetFormat: "openai", + compressionStage: "post-translation", + } + ); + assert.equal(post.compressed, true); + assert.ok(JSON.stringify(post.body).includes('"type":"image_url"')); +}); diff --git a/tests/unit/compression/omniglyph-single-mode.test.ts b/tests/unit/compression/omniglyph-single-mode.test.ts index d9ff66fb99..79accde726 100644 --- a/tests/unit/compression/omniglyph-single-mode.test.ts +++ b/tests/unit/compression/omniglyph-single-mode.test.ts @@ -3,8 +3,13 @@ import assert from "node:assert"; import { applyCompressionAsync } from "../../../open-sse/services/compression/strategySelector.ts"; import { registerBuiltinCompressionEngines } from "../../../open-sse/services/compression/engines/index.ts"; -const DENSE = "X".repeat(500) + "\n" + - Array.from({ length: 400 }, (_, i) => `const row_${i} = compute(${i * 17}, "${"v".repeat(80)}");`).join("\n"); +const DENSE = + "X".repeat(500) + + "\n" + + Array.from( + { length: 400 }, + (_, i) => `const row_${i} = compute(${i * 17}, "${"v".repeat(80)}");` + ).join("\n"); const body = () => ({ model: "claude-fable-5", max_tokens: 128, @@ -18,6 +23,7 @@ test("modo omniglyph sozinho comprime (selecionar o modo é o enable)", async () model: "claude-fable-5", supportsVision: true, providerTransport: "direct", + imageTransportFidelity: "byte-preserving", }); assert.equal(r.compressed, true); assert.ok(JSON.stringify(r.body).includes('"type":"image"'));