From f3d92aec5cdee39f42c13b66e301d9be7b81b108 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 17 Jul 2026 05:32:09 -0300 Subject: [PATCH] fix(sse): feed compression pipeline the authoritative vision capability (#7237) (#7560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chatCore.ts fed applyCompressionAsync's supportsVision option from isVisionModelId() — the deliberately-conservative model-id fragment heuristic in src/shared/constants/visionModels.ts — instead of the authoritative getResolvedModelCapabilities().supportsVision used by every other vision-aware path (e.g. the vision-bridge guardrail). gpt-5.5 is registered with supportsVision:true in modelSpecs.ts, but the fragment list has no gpt-5.x entry, so the heuristic wrongly returned false. That false reached lite.ts's replaceImageUrls(), whose gate is `supportsVision !== false`, silently stripping every image_url block before the request ever reached the executor. getResolvedModelCapabilities().supportsVision resolves to null (not false) for genuinely unknown models, which the same !== false gate already treats as preserve-by-default — matching the conservative semantics used elsewhere and avoiding the #4071/#4012 class of bug (blinding a model that can actually see). --- ...on-compression-authoritative-capability.md | 1 + open-sse/handlers/chatCore.ts | 14 ++- ...sion-authoritative-capability-7237.test.ts | 85 +++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/7237-vision-compression-authoritative-capability.md create mode 100644 tests/unit/vision-compression-authoritative-capability-7237.test.ts diff --git a/changelog.d/fixes/7237-vision-compression-authoritative-capability.md b/changelog.d/fixes/7237-vision-compression-authoritative-capability.md new file mode 100644 index 0000000000..8fa0b61e3e --- /dev/null +++ b/changelog.d/fixes/7237-vision-compression-authoritative-capability.md @@ -0,0 +1 @@ +- fix(sse): feed the compression pipeline the authoritative vision capability instead of the conservative model-id heuristic, so vision models absent from the fragment list (e.g. gpt-5.5) no longer have their image_url blocks silently stripped (#7237) diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index e134a744f8..f7c8774e66 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -112,9 +112,8 @@ import { normalizeClaudeHaikuConstraints } from "../services/claudeHaikuConstrai import { echoModelInObject } from "../services/responseModelEcho.ts"; import { stripGpt5SamplingWhenReasoning } from "../services/gpt5SamplingGuard.ts"; import { getUnsupportedParams, REGISTRY } from "../config/providerRegistry.ts"; -import { supportsMaxTokens } from "@/lib/modelCapabilities.ts"; +import { supportsMaxTokens, getResolvedModelCapabilities } from "@/lib/modelCapabilities.ts"; import { normalizeThinkingForModel } from "@/shared/constants/modelSpecs.ts"; -import { isVisionModelId } from "@/shared/constants/visionModels.ts"; import { buildErrorBody, createErrorResult, @@ -1327,7 +1326,16 @@ export async function handleChatCore({ const compressionConfig = resolveCacheAwareConfig(config, compressionInputBody, cacheCtx); const result = await applyCompressionAsync(compressionInputBody, mode, { model: effectiveModel, - supportsVision: isVisionModelId(effectiveModel), + // #7237: feed the AUTHORITATIVE capability (model spec / models.dev sync / DB + // override, with the conservative model-id fragment heuristic only as its + // last-resort fallback) instead of calling the heuristic directly here. The + // heuristic alone wrongly returned false for e.g. gpt-5.5 (registered + // supportsVision:true in modelSpecs but absent from the deliberately-conservative + // fragment list), and lite.ts's gate (`supportsVision !== false`) treated that + // false as "strip every image_url block". Resolves to `null` for genuinely unknown + // models, which is intentionally NOT `false` so the gate still preserves images. + supportsVision: getResolvedModelCapabilities({ provider, model: effectiveModel }) + .supportsVision, // Rota direta oficial ('anthropic') vs agregadores: o engine omniglyph // exige 'direct' — agregadores redimensionam imagens (medido 2026-07-06). providerTransport: provider === "anthropic" ? "direct" : "aggregator", diff --git a/tests/unit/vision-compression-authoritative-capability-7237.test.ts b/tests/unit/vision-compression-authoritative-capability-7237.test.ts new file mode 100644 index 0000000000..6801c3b676 --- /dev/null +++ b/tests/unit/vision-compression-authoritative-capability-7237.test.ts @@ -0,0 +1,85 @@ +/** + * #7237 — vision-capable models lose image_url blocks under compression. + * + * `open-sse/handlers/chatCore.ts` fed `applyCompressionAsync`'s `supportsVision` option + * from `isVisionModelId(effectiveModel)` — the deliberately-conservative model-id + * fragment heuristic in `src/shared/constants/visionModels.ts` — instead of the + * authoritative `getResolvedModelCapabilities().supportsVision` that every other + * vision-aware code path (e.g. the vision-bridge guardrail) uses. + * + * `gpt-5.5` is registered with `supportsVision: true` in `src/shared/constants/modelSpecs.ts` + * but has no gpt-5.x entry in the fragment list, so the heuristic wrongly returned `false`. + * `open-sse/services/compression/lite.ts::replaceImageUrls()` gates on + * `supportsVision !== false`, so that spurious `false` made it silently strip every + * `image_url` block from the request before it ever reached the executor. + * + * This test asserts the CORRECT, authoritative-capability-driven behavior: gpt-5.5 + * keeps its images through the lite-compression path. + */ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +import { isVisionModelId } from "../../src/shared/constants/visionModels.ts"; +import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts"; +import { replaceImageUrls } from "../../open-sse/services/compression/lite.ts"; +import { applyCompressionAsync } from "../../open-sse/services/compression/strategySelector.ts"; + +function imageBody() { + return { + messages: [ + { + role: "user", + content: [{ type: "image_url", image_url: { url: "data:image/png;base64,iVBOR" } }], + }, + ], + }; +} + +describe("#7237 vision-capable models keep their images through compression", () => { + it("documents the drift: the conservative id-fragment heuristic disagrees with the authoritative spec for gpt-5.5", () => { + assert.equal( + isVisionModelId("gpt-5.5"), + false, + "the fragment-list heuristic has no gpt-5.x entry — it is a deliberately conservative fallback, not the source of truth" + ); + assert.equal( + getResolvedModelCapabilities({ model: "gpt-5.5" }).supportsVision, + true, + "modelSpecs.ts registers gpt-5.5 with supportsVision:true — this is the authoritative source chatCore must use" + ); + }); + + it("replaceImageUrls preserves the image when fed the authoritative capability (the fixed chatCore.ts:1330 behavior)", () => { + const authoritativeSupportsVision = getResolvedModelCapabilities({ + model: "gpt-5.5", + }).supportsVision; + const result = replaceImageUrls(imageBody(), { supportsVision: authoritativeSupportsVision }); + assert.equal(result.applied, false, "the image must be KEPT, not stripped to a placeholder"); + const content = result.body.messages?.[0]?.content as Array>; + assert.equal(content[0].type, "image_url", "the block must remain a real image_url block"); + }); + + it("regresses the pre-fix bug: feeding the raw heuristic value strips the image for gpt-5.5", () => { + const buggyValue = isVisionModelId("gpt-5.5"); // false — the pre-fix chatCore.ts:1330 input + const result = replaceImageUrls(imageBody(), { supportsVision: buggyValue }); + assert.equal( + result.applied, + true, + "sanity check: this reproduces the bug shape when fed the wrong (heuristic) value" + ); + }); + + it("applyCompressionAsync end-to-end (lite mode) keeps image_url blocks for gpt-5.5 when fed the authoritative capability", async () => { + const model = "gpt-5.5"; + const supportsVision = getResolvedModelCapabilities({ model }).supportsVision; + const result = await applyCompressionAsync(imageBody(), "lite", { model, supportsVision }); + const content = (result.body as { messages: Array<{ content: unknown }> }).messages[0] + .content as Array>; + assert.equal(content[0].type, "image_url", "gpt-5.5 must keep its image_url block intact"); + assert.equal( + (content[0].image_url as Record)?.url, + "data:image/png;base64,iVBOR", + "the original data URL must survive unchanged" + ); + }); +});