Repository navigation
QVAC-27164 feat[api|bc]: support EmbeddingGemma 2 multimodal embeddings - #5464
maxim-smotrov wants to merge 14 commits into
Conversation
License compliance — cleanNo new dependency license findings in this PR. Warn-only (shadow) mode — this check does not block merges yet. Updated automatically by the canonical license compliance workflow. NOTICE presence (advisory)Missing NOTICE (advisory, does not block):
|
Review StatusCurrent Status: ❌ PENDING Pending reviews: Needs 1 Management or Team Lead, and 1 more from Management, Team Lead, or Member. |
539e642 to
9c4b45b
Compare
[Medium impact] The PR appears safe to merge; no new actionable issues remain from this review. SummaryAdds ordered text, image, and audio embedding batches while keeping existing TypeScript text calls supported.
|
cb9fb86 to
c367ca9
Compare
QVAC E2E — base run recordedBase: run 41661064462 · suite
All recorded platforms are green — nothing for |
QVAC E2E — tests changed by this PRThis smoke run additionally covers 89 test(s) that are not tagged Catalog: 624 tests · smoke: 124 · touched by this PR: 129 (40 already in smoke)
89 added test id(s)These changed files map to no test, so nothing was added for them — review by hand if they affect behaviour:
|
QVAC E2E —
|
QVAC E2E —
|
QVAC E2E —
|
QVAC E2E —
|
QVAC E2E —
|
d877ec2 to
2c66681
Compare
Signed-off-by: Maksim Smatrou <maxim.smotrov@itrexgroup.com>
Signed-off-by: Maksim Smatrou <maxim.smotrov@itrexgroup.com>
2c66681 to
e9a8bc4
Compare
lauripiisang
left a comment
There was a problem hiding this comment.
The way we do e2e is a bit different now so the implementation should be changed to match there, plus the schemas could use titles so they get better naming in generated code.
| import type { TestDefinition } from '@qvac/test-suite' | ||
|
|
||
| // Compared on the same device; no checkpoint-specific vector values are assumed. | ||
| export const embeddingGemma2Tests: TestDefinition[] = [ |
There was a problem hiding this comment.
These definitions carry no test data: params is {}, nothing reads the type: 'string' expectation, and there are no steps. The inputs, asset names, the 768 dimension, the cosine thresholds (0.999, 0.99999) and the error pattern all live in embeddinggemma2-executor.ts. So the tests only run where that executor is registered: the Python leg is skipped in platform-skips.ts, the Kotlin runner cannot run them, and no e2e test sends the regenerated Python EmbedRequest to a real model.
New e2e tests are written as declarative definitions now:
packages/sdk-e2e/README.md, "Developing new tests": prefer a declarative body; an executor is only for bodies that cannot be data..agents/skills/qv-sdk-e2e-create/SKILL.md(/qv-sdk-e2e-create): "Body and placement" says the same. "Model-output strategy" names cosine similarity against a reference vector as the case for anamed:assertion implemented in both clients, and liststypeas the last-resort fallback.references/scaffolding.mdhas the definition template.catalog/rerank-tests.tsis the closest existing example: its ranking steps are built fromparamsand checked withcompare … named: 'greaterThan', andRerankExecutorreads the sameparamson mobile.
The existing steps can express every check here:
assetwithform: 'path'forelephant.jpg,cat.jpg,sample-hi.wav(kindsimage/audio; the JS and Python runners both resolve them)call: { method: 'embed', params: { modelId: '$model', inputs: [...] } }with{ media: { type: 'filePath', value: '$elephant' } }partsprojectonembedding[0]…embedding[3]callError+errorMatchesfor the corrupt WAV (the addon rejects it with codeUnsupportedMedia), then a secondcallfor the recovery check
Missing named checks, added to runners/js/shared/step-bindings.ts and runners/python/qvac_e2e/assertions.py:
- assertion
unitVectors(with: { dimensions: 768, tolerance: 1e-3 }) - comparison
cosineAtLeast(with: { min }) for the audio determinism and text parity tests - comparison
moreSimilarThan(with: { than: '$v3' }) for the retrieval ranking
const retrievalSteps = (queries: [string, string], pairs: Array<[number, number, number]>): Step[] => [
{ useModel: { deps: ['embeddinggemma2'], as: 'model' } },
{ asset: { kind: 'image', file: 'elephant.jpg', form: 'path', as: 'elephant' } },
{ asset: { kind: 'image', file: 'cat.jpg', form: 'path', as: 'cat' } },
{
call: {
method: 'embed',
params: {
modelId: '$model',
inputs: [
['title: none | text: ', { media: { type: 'filePath', value: '$elephant' } }],
['title: none | text: ', { media: { type: 'filePath', value: '$cat' } }],
...queries
]
},
as: 'response'
}
},
{ project: { from: '$response', path: 'embedding', as: 'vectors' } },
{ assert: { on: '$vectors', named: 'lengthIs', with: { length: 4 } } },
{ assert: { on: '$vectors', named: 'unitVectors', with: { dimensions: 768 } } },
...[0, 1, 2, 3].map((i): Step => ({
project: { from: '$response', path: `embedding[${i}]`, as: `v${i}` }
})),
...pairs.map(([anchor, match, other]): Step => ({
compare: { left: `$v${anchor}`, right: `$v${match}`, named: 'moreSimilarThan', with: { than: `$v${other}` } }
}))
]With declarative bodies, the desktop-python rule in platform-skips.ts and its four skip-matrix.json entries can go. Mobile has no step interpreter, so either keep the executor for mobile only and have it read params from the definition (as RerankExecutor.ranking does), or skip mobile with a reason.
Signed-off-by: Maksim Smatrou <maxim.smotrov@itrexgroup.com>
Signed-off-by: Maksim Smatrou <maxim.smotrov@itrexgroup.com>
Signed-off-by: Maksim Smatrou <maxim.smotrov@itrexgroup.com>
Signed-off-by: Maksim Smatrou <maxim.smotrov@itrexgroup.com>
🎯 What problem does this PR solve?
SDK doesn't provide support for EmbeddingGemma 2 for multi-modal embeddings. The addon already added support in #5237.
📝 How does it solve it?
inputsbatches with text, file-path media, and base64 media while preserving the existingtextcalls.🧪 How was it tested?
💥 Breaking Changes
The generated Kotlin and Python
EmbedRequesttypes now represent two alternatives.EmbedTextRequestrequirestextand forbidsinputs.EmbedInputsRequestrequiresinputsand forbidstext.A single object with optional
textandinputswould keep the old constructors, but its Zod refinement requiring exactly one field would not appear in the exported JSON Schema. Generated clients could then accept both fields or neither. With the union, Kotlin callers must select a variant with a required input field, and Python rejects both or neither during model validation, before RPC. The cost is the generated-client migration below. ExistingQvacEmbeddings.embedconvenience methods remain unchanged.The TypeScript
embed()overloads preserve precise result types:text: stringreturns one vector, whiletext: string[]andinputsreturn a batch of vectors. These overloads do not cause the generated-client migration; the request schema union does. Existing TypeScript text calls remain supported.Kotlin
Direct construction of
EmbedRequestnow requires a request variant wrapper. The text value type is renamed fromEmbedRequestTexttoEmbedTextRequestText.BEFORE:
AFTER:
For text batches, use
EmbedTextRequestText.Variant2(texts)inside the same wrapper. For multimodal batches, useEmbedRequest.EmbedInputsRequestwrapping anEmbedInputsRequest. When inspecting a request typed asEmbedRequest, select the variant and read its fields through.value.The existing
QvacEmbeddings.embed(modelId, text)andembed(modelId, texts)convenience methods remain supported and need no caller migration.Python
The existing keyword constructor still works.
EmbedRequestis now a PydanticRootModelcontaining the selected request variant, so direct.model_id,.text, and.request_idaccess must move under.root.BEFORE:
AFTER:
For the
inputsvariant, readrequest.root.inputsafter selecting that variant. Continue passing the outerrequesttoembed(transport, request); do not replace it withrequest.root. Wire serialization remains flat, with norootorvaluefield added to the RPC payload.🔌 API Changes
Load the model with
modelConfig.projectionModelSrc, then pass text and media inputs:The result contains one vector per input.