diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index 6ac223aaab..cbec17cc19 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -349,7 +349,9 @@ export const updateSettingsSchema = z.object({ modalityBridgeVisionPrompt: z.string().max(5000).optional(), modalityBridgeVisionTimeout: z.number().int().min(1000).max(300000).optional(), modalityBridgeVisionMaxImages: z.number().int().min(1).max(20).optional(), - modalityBridgeVisionMaxChars: z.number().int().min(100).max(50000).optional(), + modalityBridgeVisionMaxChars: z + .union([z.literal(0), z.number().int().min(100).max(50000)]) + .optional(), modalityBridgeAudioEnabled: z.boolean().optional(), modalityBridgeAudioModel: z.string().max(200).optional(), modalityBridgeAudioTimeout: z.number().int().min(1000).max(300000).optional(), diff --git a/tests/unit/vision-bridge-maxchars.test.ts b/tests/unit/vision-bridge-maxchars.test.ts index 90d02eb3a0..dc39334dd1 100644 --- a/tests/unit/vision-bridge-maxchars.test.ts +++ b/tests/unit/vision-bridge-maxchars.test.ts @@ -14,6 +14,7 @@ import assert from "node:assert/strict"; const { VisionBridgeGuardrail } = await import("../../src/lib/guardrails/visionBridge.ts"); const { resolveVisionBridgeRuntimeSettings } = await import("../../src/shared/constants/modalityBridgeDefaults.ts"); +const { updateSettingsSchema } = await import("../../src/shared/validation/settingsSchemas.ts"); import type { GuardrailContext } from "../../src/lib/guardrails/base.ts"; import type { VisionModelConfig } from "../../src/lib/guardrails/visionBridgeHelpers.ts"; @@ -108,3 +109,34 @@ test("resolveVisionBridgeRuntimeSettings({}) defaults maxChars to 0 (no cap)", ( const runtime = resolveVisionBridgeRuntimeSettings({}); assert.equal(runtime.maxChars, 0); }); + +test("updateSettingsSchema accepts an explicit modalityBridgeVisionMaxChars: 0 to disable the cap", async () => { + const parsed = updateSettingsSchema.safeParse({ modalityBridgeVisionMaxChars: 0 }); + assert.equal(parsed.success, true, "explicit 0 must validate (disables the cap)"); + + const guardrail = new VisionBridgeGuardrail({ + deps: { + getSettings: async () => ({ + visionBridgeEnabled: true, + modalityBridgeVisionMaxChars: 0, + }), + callVisionModel: async (_imageDataUri: string, _config: VisionModelConfig) => + LONG_DESCRIPTION, + }, + }); + + const body = imageBody("maxchars-explicit-zero-test"); + const context: GuardrailContext = { model: TEXT_ONLY_MODEL, log: console }; + const result = await guardrail.preCall(body, context); + + const text = findImageDescriptionText(result.modifiedPayload); + assert.ok(text, "expected a [Image 1]: text part in the modified payload"); + const description = text!.slice("[Image 1]: ".length); + assert.equal(description, LONG_DESCRIPTION, "explicit 0 must disable truncation entirely"); + assert.ok(!description.endsWith("…"), "explicit 0 must never add an ellipsis"); +}); + +test("updateSettingsSchema rejects a value below the 100 floor that is not the explicit-0 sentinel", () => { + const parsed = updateSettingsSchema.safeParse({ modalityBridgeVisionMaxChars: 50 }); + assert.equal(parsed.success, false, "50 is neither the 0 sentinel nor >= 100"); +});