From 1d2906722ab3b7bab6b0aa2432b302cf4d10f5a1 Mon Sep 17 00:00:00 2001 From: yingying0906 <30721578+yingying0906@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:16:41 +0800 Subject: [PATCH 1/6] QVAC-23075 feat[api]: expose image_no_upscale in the SDK config schema Adds the idefics3-style preprocessing switch to the llamacpp completion config so a caller can override what the GGUF says. "on" rounds the image's long side up to a whole number of slices and caps it, so an image below the cap keeps its own resolution and becomes far fewer slices; "off" always stretches the long side to the cap. Unset keeps the model's own value. This is what separates the VisionPsy Flash checkpoint from the base one, whose mmprojs are otherwise indistinguishable, so a Flash checkpoint loaded without it silently runs base preprocessing. It changes the image token count, so it moves both accuracy and encode time. Additive and optional, so [api] rather than [bc]. The generated Python client is regenerated in the same commit because pr-checks-sdk-python.yml runs generate.py --check and fails the merge guard on a stale client. --- .../inference/src/schemas/llamacpp-config.ts | 12 ++++++++ .../inference/test/llm-config-schema.test.ts | 28 +++++++++++++++++++ .../qvac_sdk/_generated/models/__init__.py | 2 ++ .../qvac_sdk/_generated/models/_internal.py | 9 ++++++ packages/sdk/contract/schema.json | 5 ++++ packages/sdk/schemas/llamacpp-config.ts | 12 ++++++++ .../sdk/test/unit/llm-config-schema.test.ts | 18 ++++++++++++ 7 files changed, 86 insertions(+) diff --git a/packages/inference/src/schemas/llamacpp-config.ts b/packages/inference/src/schemas/llamacpp-config.ts index 14b736e740..3aac539804 100644 --- a/packages/inference/src/schemas/llamacpp-config.ts +++ b/packages/inference/src/schemas/llamacpp-config.ts @@ -82,6 +82,18 @@ export const llmConfigBaseSchema = z.object({ * Ignored by text-only models. Default is `"sequential"`. */ image_tile_mode: z.enum(['disabled', 'batched', 'sequential']).optional(), + /** + * idefics3-style image preprocessing rule (multimodal models only): + * - `"on"`: round the image's long side up to a whole number of slices and + * cap it, so an image smaller than the cap keeps its own resolution and + * becomes far fewer slices. + * - `"off"`: always stretch the long side to the cap. + * When unset, the model's own GGUF value is used. Ignored with a warning by + * models that do not use idefics3-style preprocessing. Changes the number of + * image tokens, and therefore both accuracy and encode time, so a checkpoint + * whose GGUF omits the key needs this set to preprocess correctly. + */ + image_no_upscale: z.enum(['on', 'off']).optional(), /** * Run the multimodal projector (mmproj / vision encoder) on the GPU * (multimodal models only). `true` forces GPU, `false` forces CPU. When diff --git a/packages/inference/test/llm-config-schema.test.ts b/packages/inference/test/llm-config-schema.test.ts index d25284f856..5d56697811 100644 --- a/packages/inference/test/llm-config-schema.test.ts +++ b/packages/inference/test/llm-config-schema.test.ts @@ -150,6 +150,34 @@ test('llmConfigSchema: explicit image_tile_mode overrides the default', (t) => { if (result.success) t.is(result.data.image_tile_mode, 'batched') }) +test('llmConfigBaseSchema: accepts valid image_no_upscale values', (t) => { + t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: 'on' }).success, true) + t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: 'off' }).success, true) +}) + +test('llmConfigBaseSchema: rejects invalid image_no_upscale values', (t) => { + t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: true }).success, false) + t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: 1 }).success, false) + t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: 'yes' }).success, false) +}) + +// Unset must stay unset. The addon reads absence as fabric's -1 sentinel, meaning +// "use the model's own GGUF value"; a default here would force one rule on every +// model and silently change preprocessing for existing callers. +test('llmConfigBaseSchema: image_no_upscale is optional and has no default', (t) => { + t.is(llmConfigBaseSchema.safeParse({}).success, true) + const result = llmConfigSchema.safeParse({}) + t.is(result.success, true) + if (result.success) t.is(result.data.image_no_upscale, undefined) +}) + +// The regression this guards: load-model.ts validates modelConfig with +// llmConfigBaseSchema.strict(), so a field present in the SDK copy of this schema but +// missing here is rejected before it ever reaches the addon. +test('loadBuiltinModelOptions: strict validation admits image_no_upscale', (t) => { + t.is(llmConfigBaseSchema.strict().safeParse({ image_no_upscale: 'on' }).success, true) +}) + test('llmConfigBaseSchema: accepts mmproj-use-gpu boolean', (t) => { const enabled = llmConfigBaseSchema.safeParse({ 'mmproj-use-gpu': true }) t.is(enabled.success, true) diff --git a/packages/sdk-python/src/tetherto/qvac_sdk/_generated/models/__init__.py b/packages/sdk-python/src/tetherto/qvac_sdk/_generated/models/__init__.py index bc0b3e044a..b9633ff5de 100644 --- a/packages/sdk-python/src/tetherto/qvac_sdk/_generated/models/__init__.py +++ b/packages/sdk-python/src/tetherto/qvac_sdk/_generated/models/__init__.py @@ -482,6 +482,7 @@ LoadModelSrcRequestLlamacppCompletion, LoadModelSrcRequestLlamacppCompletionDelegate, LoadModelSrcRequestLlamacppCompletionModelConfig, + LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale, LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode, LoadModelSrcRequestLlamacppCompletionModelConfigMainGpu, LoadModelSrcRequestLlamacppCompletionModelConfigProjectionModelSrc, @@ -1237,6 +1238,7 @@ "LoadModelSrcRequestLlamacppCompletion", "LoadModelSrcRequestLlamacppCompletionDelegate", "LoadModelSrcRequestLlamacppCompletionModelConfig", + "LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale", "LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode", "LoadModelSrcRequestLlamacppCompletionModelConfigMainGpu", "LoadModelSrcRequestLlamacppCompletionModelConfigProjectionModelSrc", diff --git a/packages/sdk-python/src/tetherto/qvac_sdk/_generated/models/_internal.py b/packages/sdk-python/src/tetherto/qvac_sdk/_generated/models/_internal.py index 5ce2044189..e824208536 100644 --- a/packages/sdk-python/src/tetherto/qvac_sdk/_generated/models/_internal.py +++ b/packages/sdk-python/src/tetherto/qvac_sdk/_generated/models/_internal.py @@ -6454,6 +6454,11 @@ class LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode(Enum): sequential = "sequential" +class LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale(Enum): + on = "on" + off = "off" + + class LoadModelSrcRequestLlamacppCompletionModelConfig(GeneratedBaseModel): ctx_size: float | None = None temp: Annotated[float | None, Field(ge=0.0, le=2.0)] = None @@ -6509,6 +6514,10 @@ class LoadModelSrcRequestLlamacppCompletionModelConfig(GeneratedBaseModel): LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode | None, Field(title="LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode"), ] = None + image_no_upscale: Annotated[ + LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale | None, + Field(title="LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale"), + ] = None mmproj_use_gpu: Annotated[bool | None, Field(alias="mmproj-use-gpu")] = None diff --git a/packages/sdk/contract/schema.json b/packages/sdk/contract/schema.json index 4be0a96325..3ccaee5c86 100644 --- a/packages/sdk/contract/schema.json +++ b/packages/sdk/contract/schema.json @@ -8076,6 +8076,11 @@ "enum": ["disabled", "batched", "sequential"], "title": "LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode" }, + "image_no_upscale": { + "type": "string", + "enum": ["on", "off"], + "title": "LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale" + }, "mmproj-use-gpu": { "type": "boolean" } diff --git a/packages/sdk/schemas/llamacpp-config.ts b/packages/sdk/schemas/llamacpp-config.ts index 839d9da03b..ba3440fb64 100644 --- a/packages/sdk/schemas/llamacpp-config.ts +++ b/packages/sdk/schemas/llamacpp-config.ts @@ -82,6 +82,18 @@ export const llmConfigBaseSchema = z.object({ * Ignored by text-only models. Default is `"sequential"`. */ image_tile_mode: z.enum(['disabled', 'batched', 'sequential']).optional(), + /** + * idefics3-style image preprocessing rule (multimodal models only): + * - `"on"`: round the image's long side up to a whole number of slices and + * cap it, so an image smaller than the cap keeps its own resolution and + * becomes far fewer slices. + * - `"off"`: always stretch the long side to the cap. + * When unset, the model's own GGUF value is used. Ignored with a warning by + * models that do not use idefics3-style preprocessing. Changes the number of + * image tokens, and therefore both accuracy and encode time, so a checkpoint + * whose GGUF omits the key needs this set to preprocess correctly. + */ + image_no_upscale: z.enum(['on', 'off']).optional(), /** * Run the multimodal projector (mmproj / vision encoder) on the GPU * (multimodal models only). `true` forces GPU, `false` forces CPU. When diff --git a/packages/sdk/test/unit/llm-config-schema.test.ts b/packages/sdk/test/unit/llm-config-schema.test.ts index d25284f856..6c04f6f7b5 100644 --- a/packages/sdk/test/unit/llm-config-schema.test.ts +++ b/packages/sdk/test/unit/llm-config-schema.test.ts @@ -150,6 +150,24 @@ test('llmConfigSchema: explicit image_tile_mode overrides the default', (t) => { if (result.success) t.is(result.data.image_tile_mode, 'batched') }) +test('llmConfigBaseSchema: accepts valid image_no_upscale values', (t) => { + t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: 'on' }).success, true) + t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: 'off' }).success, true) +}) + +test('llmConfigBaseSchema: rejects invalid image_no_upscale values', (t) => { + t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: 'maybe' }).success, false) + t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: true }).success, false) +}) + +// No default: unset must stay unset so the addon keeps the model's own GGUF value +// rather than being forced to base preprocessing. +test('llmConfigSchema: image_no_upscale has no default', (t) => { + const result = llmConfigSchema.safeParse({}) + t.is(result.success, true) + if (result.success) t.is(result.data.image_no_upscale, undefined) +}) + test('llmConfigBaseSchema: accepts mmproj-use-gpu boolean', (t) => { const enabled = llmConfigBaseSchema.safeParse({ 'mmproj-use-gpu': true }) t.is(enabled.success, true) From c05894d617e41c5f3a4b90edf4a9cc5e87c8a150 Mon Sep 17 00:00:00 2001 From: Maksim Smatrou Date: Tue, 18 Aug 2026 14:41:09 +0200 Subject: [PATCH 2/6] test: added VisionPsy model test and replaced SmolVLM2 is existing tests --- .../sdk/e2e/tests/batch-completion-tests.ts | 2 +- packages/sdk/e2e/tests/desktop/consumer.ts | 24 ++++++--- packages/sdk/e2e/tests/electron/consumer.ts | 26 +++++++--- packages/sdk/e2e/tests/mobile/consumer.ts | 24 ++++++--- .../tests/mobile/executors/vision-executor.ts | 52 +++++++++++++++++++ .../shared/executors/node/vision-executor.ts | 52 +++++++++++++++++++ packages/sdk/e2e/tests/vision-tests.ts | 23 +++++++- 7 files changed, 182 insertions(+), 21 deletions(-) diff --git a/packages/sdk/e2e/tests/batch-completion-tests.ts b/packages/sdk/e2e/tests/batch-completion-tests.ts index 7fa49301ea..deb560ead9 100644 --- a/packages/sdk/e2e/tests/batch-completion-tests.ts +++ b/packages/sdk/e2e/tests/batch-completion-tests.ts @@ -80,7 +80,7 @@ const markerDeterministic: GenerationParams = { ...deterministic, predict: 32 } const visionDeterministic: GenerationParams = { temp: 0, seed: 42, - predict: 48 + predict: 128 } const ELEPHANT_IMAGE_TERMS = ['elephant', 'tusk', 'trunk'] diff --git a/packages/sdk/e2e/tests/desktop/consumer.ts b/packages/sdk/e2e/tests/desktop/consumer.ts index 7c39126eca..61e02b351f 100644 --- a/packages/sdk/e2e/tests/desktop/consumer.ts +++ b/packages/sdk/e2e/tests/desktop/consumer.ts @@ -34,8 +34,8 @@ import { PI05_BASE_Q_AGGRESSIVE, GROOT_Q5_VF16, GROOT_MULTI_Q5_VF16, - SMOLVLM2_500M_MULTIMODAL_Q8_0, - MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0, + VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M, + MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0, FLUX_2_KLEIN_4B_Q4_0, FLUX_2_KLEIN_4B_VAE, QWEN3_4B_Q4_K_M, @@ -443,21 +443,33 @@ resources.define('bci', { }) resources.define('vision', { - constant: SMOLVLM2_500M_MULTIMODAL_Q8_0, + constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M, type: 'llamacpp-completion', config: { ctx_size: 4096, - projectionModelSrc: MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0 + image_no_upscale: 'on', + projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0 } }) resources.define('vision-batch', { - constant: SMOLVLM2_500M_MULTIMODAL_Q8_0, + constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M, type: 'llamacpp-completion', config: { ctx_size: 2048, parallel: 2, - projectionModelSrc: MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0 + image_no_upscale: 'on', + projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0 + } +}) + +resources.define('vision-upscale', { + constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M, + type: 'llamacpp-completion', + config: { + ctx_size: 4096, + image_no_upscale: 'off', + projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0 } }) diff --git a/packages/sdk/e2e/tests/electron/consumer.ts b/packages/sdk/e2e/tests/electron/consumer.ts index 2b9144c04c..2be430838f 100644 --- a/packages/sdk/e2e/tests/electron/consumer.ts +++ b/packages/sdk/e2e/tests/electron/consumer.ts @@ -43,8 +43,8 @@ import { PARAKEET_INDIC_CONFORMER_CTC_Q4_0, PARAKEET_SORTFORMER_4SPK_V2_1_Q4_0, PARAKEET_EOU_120M_V1_Q4_0, - SMOLVLM2_500M_MULTIMODAL_Q8_0, - MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0, + VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M, + MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0, QWEN3_5_0_8B_MULTIMODAL_Q4_K_M, GEMMA4_2B_MULTIMODAL_Q4_K_M, BCI_WINDOWED @@ -395,21 +395,33 @@ resources.define('bci', { }) resources.define('vision', { - constant: SMOLVLM2_500M_MULTIMODAL_Q8_0, + constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M, type: 'llamacpp-completion', config: { - ctx_size: 1024, - projectionModelSrc: MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0 + ctx_size: 4096, + image_no_upscale: 'on', + projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0 } }) resources.define('vision-batch', { - constant: SMOLVLM2_500M_MULTIMODAL_Q8_0, + constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M, type: 'llamacpp-completion', config: { ctx_size: 2048, parallel: 2, - projectionModelSrc: MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0 + image_no_upscale: 'on', + projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0 + } +}) + +resources.define('vision-upscale', { + constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M, + type: 'llamacpp-completion', + config: { + ctx_size: 4096, + image_no_upscale: 'off', + projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0 } }) diff --git a/packages/sdk/e2e/tests/mobile/consumer.ts b/packages/sdk/e2e/tests/mobile/consumer.ts index 0d52bde85d..4a87a420d7 100644 --- a/packages/sdk/e2e/tests/mobile/consumer.ts +++ b/packages/sdk/e2e/tests/mobile/consumer.ts @@ -30,8 +30,8 @@ import { PARAKEET_CTC_0_6B_Q4_0, PARAKEET_SORTFORMER_4SPK_V2_1_Q4_0, PARAKEET_EOU_120M_V1_Q4_0, - SMOLVLM2_500M_MULTIMODAL_Q8_0, - MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0, + VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M, + MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0, SMOLVLA_LIBERO_VISION_Q8 } from '@qvac/sdk' import { ResourceManager } from '../shared/resource-manager.js' @@ -376,21 +376,33 @@ resources.define('parakeet-eou', { }) resources.define('vision', { - constant: SMOLVLM2_500M_MULTIMODAL_Q8_0, + constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M, type: 'llamacpp-completion', config: { ctx_size: 4096, - projectionModelSrc: MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0 + image_no_upscale: 'on', + projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0 } }) resources.define('vision-batch', { - constant: SMOLVLM2_500M_MULTIMODAL_Q8_0, + constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M, type: 'llamacpp-completion', config: { ctx_size: 2048, parallel: 2, - projectionModelSrc: MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0 + image_no_upscale: 'on', + projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0 + } +}) + +resources.define('vision-upscale', { + constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M, + type: 'llamacpp-completion', + config: { + ctx_size: 4096, + image_no_upscale: 'off', + projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0 } }) diff --git a/packages/sdk/e2e/tests/mobile/executors/vision-executor.ts b/packages/sdk/e2e/tests/mobile/executors/vision-executor.ts index edf2a92fe0..8fc8cd7e8f 100644 --- a/packages/sdk/e2e/tests/mobile/executors/vision-executor.ts +++ b/packages/sdk/e2e/tests/mobile/executors/vision-executor.ts @@ -16,6 +16,9 @@ export class MobileVisionExecutor extends ModelAssetExecutor protected handlers = Object.fromEntries( visionTests.map((test) => { + if (test.testId === 'vision-image-no-upscale') { + return [test.testId, this.imageNoUpscale.bind(this)] + } if (test.testId.endsWith('-streaming')) { return [test.testId, this.streaming.bind(this)] } @@ -33,6 +36,55 @@ export class MobileVisionExecutor extends ModelAssetExecutor super(resources) } + private async getPromptTokens(modelId: string, params: VisionParams) { + const history = await this.resolveAttachments(params.history) + return callWhenAddonIdle(async () => { + const result = completion({ + modelId, + history, + stream: false, + ...(params.generationParams && { generationParams: params.generationParams }) + } as never) + await result.text + const stats = await result.stats + if (typeof stats?.promptTokens !== 'number' || stats.promptTokens <= 0) { + throw new Error(`Completion stats missing promptTokens. Got: ${JSON.stringify(stats)}`) + } + return stats.promptTokens + }) + } + + async imageNoUpscale(params: unknown, _expectation: Expectation): Promise { + const p = params as VisionParams + + try { + const noUpscaleModelId = await this.resources.ensureLoaded('vision') + const noUpscaleTokens = await this.getPromptTokens(noUpscaleModelId, p) + + await this.resources.evict('vision') + + const upscaleModelId = await this.resources.ensureLoaded('vision-upscale') + const upscaleTokens = await this.getPromptTokens(upscaleModelId, p) + + if (noUpscaleTokens * 2 >= upscaleTokens) { + return { + passed: false, + output: + `image_no_upscale on (${noUpscaleTokens}) should use less than half the prompt tokens ` + + `of off (${upscaleTokens})` + } + } + + return { + passed: true, + output: `image_no_upscale reduced prompt tokens from ${upscaleTokens} to ${noUpscaleTokens}` + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + return { passed: false, output: `image_no_upscale comparison failed: ${errorMsg}` } + } + } + private async loadImageAssets() { if (!this.imageAssets) { // @ts-ignore - assets.ts is generated at consumer build time diff --git a/packages/sdk/e2e/tests/shared/executors/node/vision-executor.ts b/packages/sdk/e2e/tests/shared/executors/node/vision-executor.ts index 57aa6715be..f37d235660 100644 --- a/packages/sdk/e2e/tests/shared/executors/node/vision-executor.ts +++ b/packages/sdk/e2e/tests/shared/executors/node/vision-executor.ts @@ -16,6 +16,9 @@ export class VisionExecutor extends AbstractModelExecutor { protected handlers = Object.fromEntries( visionTests.map((test) => { + if (test.testId === 'vision-image-no-upscale') { + return [test.testId, this.imageNoUpscale.bind(this)] + } if (test.testId.endsWith('-streaming')) { return [test.testId, this.streaming.bind(this)] } @@ -26,6 +29,55 @@ export class VisionExecutor extends AbstractModelExecutor { }) ) as never + private async getPromptTokens(modelId: string, params: VisionParams) { + const history = this.resolveAttachments(params.history) + return callWhenAddonIdle(async () => { + const result = completion({ + modelId, + history, + stream: false, + ...(params.generationParams && { generationParams: params.generationParams }) + } as never) + await result.text + const stats = await result.stats + if (typeof stats?.promptTokens !== 'number' || stats.promptTokens <= 0) { + throw new Error(`Completion stats missing promptTokens. Got: ${JSON.stringify(stats)}`) + } + return stats.promptTokens + }) + } + + async imageNoUpscale(params: unknown, _expectation: Expectation): Promise { + const p = params as VisionParams + + try { + const noUpscaleModelId = await this.resources.ensureLoaded('vision') + const noUpscaleTokens = await this.getPromptTokens(noUpscaleModelId, p) + + await this.resources.evict('vision') + + const upscaleModelId = await this.resources.ensureLoaded('vision-upscale') + const upscaleTokens = await this.getPromptTokens(upscaleModelId, p) + + if (noUpscaleTokens * 2 >= upscaleTokens) { + return { + passed: false, + output: + `image_no_upscale on (${noUpscaleTokens}) should use less than half the prompt tokens ` + + `of off (${upscaleTokens})` + } + } + + return { + passed: true, + output: `image_no_upscale reduced prompt tokens from ${upscaleTokens} to ${noUpscaleTokens}` + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + return { passed: false, output: `image_no_upscale comparison failed: ${errorMsg}` } + } + } + private resolveAttachments(history: VisionParams['history']) { return history.map((msg) => { if (!msg.attachments?.length) return msg diff --git a/packages/sdk/e2e/tests/vision-tests.ts b/packages/sdk/e2e/tests/vision-tests.ts index b8f8926b02..472eb47543 100644 --- a/packages/sdk/e2e/tests/vision-tests.ts +++ b/packages/sdk/e2e/tests/vision-tests.ts @@ -61,6 +61,26 @@ export const visionStats = createVisionTest( { generationParams: { temp: 0, seed: 42 } } ) +export const visionImageNoUpscale: TestDefinition = { + testId: 'vision-image-no-upscale', + params: { + history: [ + { + role: 'user', + content: 'Describe this image briefly.', + attachments: [{ path: 'shared-test-data/images/small-64.jpg' }] + } + ], + generationParams: { temp: 0, top_k: 1, seed: 42, predict: 8 } + }, + expectation: { validation: 'function', fn: () => true }, + metadata: { + category: 'vision', + dependency: 'vision', + estimatedDurationMs: 120000 + } +} + export const visionFormatPng = createVisionTest( 'vision-format-png', 'Describe this image.', @@ -103,7 +123,7 @@ export const visionTextExtraction = createVisionTest( 'Read the text in this image. Reply with only the text.', 'sign.jpg', { validation: 'contains-all', contains: ['hello'] }, - { generationParams: { temp: 0, top_k: 1, seed: 42, predict: 16 } } + { generationParams: { temp: 0, top_k: 1, seed: 42, predict: 128 } } ) export const visionSceneUnderstanding = createVisionTest( @@ -205,6 +225,7 @@ export const visionTests = [ visionBasic, visionStreaming, visionStats, + visionImageNoUpscale, visionFormatPng, visionFormatWebp, visionLargeImage, From 2ff84654c25fd870c8f63b2739debc4b70d0c143 Mon Sep 17 00:00:00 2001 From: Maksim Smatrou Date: Tue, 18 Aug 2026 14:41:22 +0200 Subject: [PATCH 3/6] chore: bump LLM addon version --- packages/sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c85dc5bb7f..f5ce2558f0 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -215,7 +215,7 @@ "@qvac/embed-llamacpp": "^0.32.0", "@qvac/error": "^0.1.1", "@qvac/langdetect-text": "^0.1.2", - "@qvac/llm-llamacpp": "^0.43.0", + "@qvac/llm-llamacpp": "^0.44.0", "@qvac/logging": "^0.1.0", "@qvac/ocr-ggml": "^0.16.0", "@qvac/rag": "^0.6.4", From 135716910bced7c614d9ac4b7366b91d9340c4e2 Mon Sep 17 00:00:00 2001 From: Maksim Smatrou Date: Tue, 18 Aug 2026 16:05:16 +0200 Subject: [PATCH 4/6] chore: bump addon deps to use same fabric version --- packages/sdk/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 1daebf2d56..00676ab1cd 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -209,20 +209,20 @@ "@qvac/audiogen-ggml": "^0.1.0", "@qvac/asr-ggml": "^0.3.0", "@qvac/bci-whispercpp": "^0.7.1", - "@qvac/classification-ggml": "^0.18.0", + "@qvac/classification-ggml": "^0.19.0", "@qvac/decoder-audio": "^0.5.0", "@qvac/diffusion-cpp": "^0.17.0", - "@qvac/embed-llamacpp": "^0.32.0", + "@qvac/embed-llamacpp": "^0.33.0", "@qvac/error": "^0.1.1", "@qvac/langdetect-text": "^0.1.2", "@qvac/llm-llamacpp": "^0.44.0", "@qvac/logging": "^0.1.0", - "@qvac/ocr-ggml": "^0.16.0", + "@qvac/ocr-ggml": "^0.17.0", "@qvac/rag": "^0.6.4", "@qvac/registry-client": "^0.6.1", - "@qvac/translation-nmtcpp": "^0.8.0", + "@qvac/translation-nmtcpp": "^0.9.0", "@qvac/tts-ggml": "^0.7.0", - "@qvac/vla-ggml": "^0.19.0", + "@qvac/vla-ggml": "^0.20.0", "bare-abort-controller": "^1.0.0", "bare-cpu-info": "0.1.1", "bare-crypto": "^1.15.0", From 33dc12e2ceb6c089629ae968dc5f0b881bf2f382 Mon Sep 17 00:00:00 2001 From: Maksim Smatrou Date: Wed, 19 Aug 2026 00:19:54 +0200 Subject: [PATCH 5/6] chore: bump classification-ggml version --- packages/sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 362dbd6201..5799f37abe 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -209,7 +209,7 @@ "@qvac/audiogen-ggml": "^0.2.1", "@qvac/asr-ggml": "^0.3.0", "@qvac/bci-whispercpp": "^0.7.1", - "@qvac/classification-ggml": "^0.19.0", + "@qvac/classification-ggml": "^0.19.1", "@qvac/decoder-audio": "^0.5.0", "@qvac/diffusion-cpp": "^0.17.0", "@qvac/embed-llamacpp": "^0.33.0", From 13da0f714e74a3ba226acf7ebdefc8e1a7733e6b Mon Sep 17 00:00:00 2001 From: Maksim Smatrou Date: Wed, 19 Aug 2026 17:14:32 +0200 Subject: [PATCH 6/6] chore: tts version bump --- packages/sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 5799f37abe..e3ff475990 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -221,7 +221,7 @@ "@qvac/rag": "^0.6.4", "@qvac/registry-client": "^0.6.1", "@qvac/translation-nmtcpp": "^0.9.0", - "@qvac/tts-ggml": "^0.7.0", + "@qvac/tts-ggml": "^0.7.4", "@qvac/vla-ggml": "^0.20.0", "bare-abort-controller": "^1.0.0", "bare-cpu-info": "0.1.1",