Skip to content

QVAC-17481 feat[api]: integrate @qvac/classification-ggml into SDK#2236

Merged
RamazTs merged 13 commits into
mainfrom
qvac-17481-classification-ggml-sdk-v2
May 26, 2026
Merged

QVAC-17481 feat[api]: integrate @qvac/classification-ggml into SDK#2236
RamazTs merged 13 commits into
mainfrom
qvac-17481-classification-ggml-sdk-v2

Conversation

@olyasir

@olyasir olyasir commented May 25, 2026

Copy link
Copy Markdown
Contributor

🎯 What problem does this PR solve?

  • Subtask of QVAC-17481. The @qvac/classification-ggml addon (ImageClassifier, bundled MobileNetV3-Small that returns food / report / other) shipped in 0.1.0, but the SDK had no way to load or run it — consumers had to depend on the addon directly and bypass loadModel / unloadModel, the plugin registry, the worker lifecycle, and the cross-runtime client surface.
  • Closes the DoD: an end-to-end SDK consumer can now loadModel({ modelType: "classification" }) and run classify({ modelId, image }); the model uses the GGUF bundled inside the addon package (no download required), and the existing per-runtime loadModel / RPC / Pear / mobile-bundle plumbing handles it the same way it handles every other built-in addon.

Reopened from #2056 after the May-24 history rewrite invalidated the original branch. Foundational plugin code recovered verbatim from @DmitryMalishev's original commit; catalog wirings re-applied against the new main (which had since added parakeet-transcription, ggml-vla, etc.).

📝 How does it solve it?

  • Built-in plugin under server/bare/plugins/ggml-classification/ (plugin.ts + ops/classify.ts) following the same pattern as onnx-ocr / ggml-vla. The plugin wraps ImageClassifier and exposes a single streaming handler (classify) declared with cancel: { scope: "none" } — classification is a single forward pass with no addon-side cancel surface. skipPrimaryModelPathValidation: true because the model is bundled inside the addon, so loadModel does not require a modelSrc.
  • classify() client API under client/api/classify.ts — accepts a Uint8Array image (JPEG/PNG, or raw RGB with width/height/channels: 3), encodes to base64 for RPC transport, and returns ClassificationResult[] ({ label, confidence }, sorted descending).
  • Top-level RPC type "classify" routed through a new server/rpc/handlers/classify.ts (uses the shared dispatchPluginStream helper) and registered in handler-registry.ts. This makes classify() work with the same RPC layer the other built-in clients use, not just invokePluginStream.
  • loadModel supports optional modelSrc for classification — the union arm in loadModelSrcRequestSchema plus the corresponding parser arm in loadModelOptionsBaseSchema both accept the empty modelSrc case. Reuse with a custom GGUF works too: pass modelSrc: "/path/to/my-classifier.gguf" and modelConfig.modelPath becomes the source of truth.
  • topK flows through load config — the plugin wraps the addon so a load-time topK (loadModel({ modelType: "classification", modelConfig: { topK: 3 } })) becomes the default for every classify() on that model, while per-call topK overrides via the standard { ...defaults, ...opts } precedence.
  • New schemas: classificationConfigSchema, classifyRequestSchema, classifyResponseSchema, classificationResultSchema, plus the ClassifyClientParams / ClassificationResult types.
  • Catalog wiring (in lock-step with the existing built-ins): ModelType.ggmlClassification + "classification" alias, classificationModelTypeSchema, ENGINE_TO_ADDON / LEGACY_ENGINE_TO_CANONICAL entries, modelRegistryEntryAddonSchema + modelRegistryEngineSchema enum extensions, modelInfoSchema.addon enum extension, CANONICAL_TO_ALIAS + MODEL_CONFIG_SCHEMAS entries, requestSchema / responseSchema union extensions.
  • Plugin / addon constants: PLUGIN_CLASSIFICATION = "@qvac/sdk/ggml-classification/plugin" added to SDK_DEFAULT_PLUGINS; ADDON_CLASSIFICATION = "@qvac/classification-ggml" added to the addon-constants block.
  • Pear bundler integration: pear/pre.ts BUILTIN_PLUGINS + BUILTIN_PLUGIN_EXPORTS updated so generated Pear workers import and register classificationPlugin from the default-plugin set.
  • Package export: ./ggml-classification/plugin exposed in package.json exports.
  • Adds dependency @qvac/classification-ggml: ^0.1.0 (now published; was previously file:.. during local development).

🧪 How was it tested?

  • Unit tests — 33 new tests across three files:
    • test/unit/classification-schemas.test.ts (25 tests): config / result / request / response schemas including the strict-unknown-key rejection, raw-RGB channels: 3 constraint, loadModelSrcRequestSchema + loadModelOptionsBaseSchema integration (canonical + alias paths), modelInfoSchema.addon enum coverage, plugin registration + dispatch through the real handlePluginInvoke, and classify op base64 round-trip + model.classify() opts forwarding + missing-method error path.
    • test/unit/load-model-schema.test.ts (3 new cases): loadModelSrcRequestSchema accepts classification load with empty modelSrc (bundled weights), with modelConfig.topK, and with a custom GGUF modelSrc.
    • test/unit/plugin-cancel-capability.test.ts (1 truth-table row): classify: { scope: "none" }, guards against silent regressions where a future plugin tweak forgets to keep cancel in sync with the addon. Validated against pluginHandlerDefinitionRuntimeSchema.
    • All pass under bun run test:unit; full unit suite stays green.
  • E2E tests under tests-qvac/:
    • tests-qvac/tests/classification-tests.ts defines 4 cases:
      • classification-results-shape (smoke): non-empty {label, confidence} array, every confidence in [0, 1], sorted descending.
      • classification-confidence-sum: softmax probabilities sum to ≈1 within 1e-3.
      • classification-topk (smoke): topK: 1 truncates to exactly one result.
      • classification-invalid-image: 4-byte garbage payload rejects cleanly, and a follow-up valid classify() proves the addon does not wedge on the error path.
    • tests-qvac/tests/desktop/executors/classification-executor.ts wires those to the SDK's classify() against the bundled MobileNetV3-Small using tests-qvac/assets/images/elephant.jpg (already in repo).
    • consumer.ts registers the "classification" resource and the executor; test-definitions.ts adds classificationTests to the master list so the consumer-desktop test runner actually picks them up.
  • ResourceManager.ModelDefinition.constant is now optional, with addConstant / ensureLoaded guarded so addons that ship bundled weights (like classification) can be registered without a registry constant or pre-download entry.
  • bun run lint + bun run build clean against upstream/main; tests-qvac typechecks clean against the local SDK snapshot.

🔌 API Changes

Additive only — no breaking changes:

import {
  loadModel,
  unloadModel,
  classify,
  PLUGIN_CLASSIFICATION,
  type ClassifyClientParams,
  type ClassificationResult,
} from "@qvac/sdk";

// Load with bundled MobileNetV3-Small — no `modelSrc` needed.
const modelId = await loadModel({
  modelType: "classification",          // alias for "ggml-classification"
  modelConfig: { topK: 3 },             // optional load-time default
});

// Or load a custom GGUF classifier.
const customId = await loadModel({
  modelType: "classification",
  modelSrc: "/abs/path/to/my-classifier.gguf",
});

// Classify a JPEG/PNG buffer.
const jpeg = fs.readFileSync("photo.jpg");
const results = await classify({ modelId, image: jpeg });
// → [ { label: "food", confidence: 0.91 }, { label: "other", confidence: 0.05 }, ... ]

// Per-call `topK` overrides the load-time default.
const topOne = await classify({ modelId, image: jpeg, topK: 1 });

// Raw RGB bytes (skip JPEG/PNG decode).
const raw = await classify({
  modelId,
  image: rgbBytes,
  width: 224,
  height: 224,
  channels: 3,
});

await unloadModel({ modelId, clearStorage: false });

See packages/sdk/examples/classification/classify-image.ts for the full demo.

Review comments addressed (carried from #2056)

All four points raised by @maxim-smotrov on the original PR are fixed in this revision:

  1. Missing loadModelSrcRequestSchema armloadClassificationModelRequestSchema now in the union, so loadModel({ modelType: "classification" }) parses through requestSchema.parse before transport.
  2. Missing top-level RPC handlerserver/rpc/handlers/classify.ts dispatches via dispatchPluginStream, registered in handler-registry.ts as { type: "stream" }.
  3. topK in load config silently ignored — the plugin wraps ImageClassifier so the load-time topK becomes the default for classify() calls; per-call topK still overrides.
  4. Pear bundler missing entrypear/pre.ts BUILTIN_PLUGINS and BUILTIN_PLUGIN_EXPORTS both include the classification plugin; generated Pear workers now import and register it.

@olyasir olyasir requested review from a team as code owners May 25, 2026 06:52

@simon-iribarren simon-iribarren left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI Status

Red and blocking. validate-pr fails because the [api] PR body does not include a fenced code block showing new API usage. SDK checks and CodeQL pass; test/build dispatch jobs are skipped.

API Surface & Tagging

[api] is only appropriate if this PR exposes classification through the SDK. The current body does not satisfy the required API evidence, and the diff appears to add only the addon dependency rather than wiring a public SDK classification surface.

Findings

  • Blocking: this is currently a dependency-only change, not an SDK integration. Adding @qvac/classification-ggml to packages/sdk/package.json does not expose model types/aliases, plugin exports, schemas, client API, default registration, docs, or tests. Users still cannot call a classification SDK API from @qvac/sdk based on this diff.

  • Blocking: [api] body evidence is missing, and CI is already failing on that validator error. Please include a fenced usage example for the actual SDK API once the integration is wired.

  • Major: no SDK tests or docs cover the claimed integration. The PR should prove model constants/plugin registration/runtime loading/client behavior, or narrow the title/body to the actual dependency maintenance change.

Recommendation

Request changes. Red PR validation alone blocks approval, and the implementation does not currently deliver the claimed SDK API integration.

@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Tier-based Approval Status

**PR Tier:** TIER1

**Current Status:** ✅ APPROVED

**Requirements:**
- 1 Team Member approval ✅ (1/1)
- 1 Team Lead OR Management approval ✅ (1/1)



---
*This comment is automatically updated when reviews change.*

@RamazTs RamazTs added tier1 test-e2e-smoke Triggers smoke e2e test suite [Currently SDK-only] verified Authorize secrets / label-gate in PR workflows labels May 26, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@RamazTs RamazTs removed the test-e2e-smoke Triggers smoke e2e test suite [Currently SDK-only] label May 26, 2026
@RamazTs RamazTs added the test-e2e-smoke Triggers smoke e2e test suite [Currently SDK-only] label May 26, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

ishanvohra2
ishanvohra2 previously approved these changes May 26, 2026
maxim-smotrov
maxim-smotrov previously approved these changes May 26, 2026
NamelsKing
NamelsKing previously approved these changes May 26, 2026
…cation-ggml-sdk-v2

# Conflicts:
#	packages/sdk/client/api/index.ts
#	packages/sdk/index.ts
ishanvohra2
ishanvohra2 previously approved these changes May 26, 2026
NamelsKing
NamelsKing previously approved these changes May 26, 2026
maxim-smotrov
maxim-smotrov previously approved these changes May 26, 2026
@RamazTs

RamazTs commented May 26, 2026

Copy link
Copy Markdown
Contributor

/review

2 similar comments
@RamazTs

RamazTs commented May 26, 2026

Copy link
Copy Markdown
Contributor

/review

@RamazTs

RamazTs commented May 26, 2026

Copy link
Copy Markdown
Contributor

/review

…cation-ggml-sdk-v2

# Conflicts:
#	packages/sdk/e2e/tests/classification-tests.ts
#	packages/sdk/e2e/tests/desktop/executors/classification-executor.ts
#	packages/sdk/e2e/tests/mobile/executors/classification-executor.ts
@kinsta

kinsta Bot commented May 26, 2026

Copy link
Copy Markdown

Preview deployments for qvac-docs-staging ⚡️

Status Branch preview Commit preview
🔁 Deploying... N/A N/A

Commit: 72f028f3a305548b97e587c3cf761d7a1a001d7b

Deployment ID: 90e370bd-8a38-4d1c-bf58-a51d091a6cea

Static site name: qvac-docs-staging-fazwv

@RamazTs RamazTs merged commit 0a4fbc4 into main May 26, 2026
27 checks passed
@RamazTs RamazTs deleted the qvac-17481-classification-ggml-sdk-v2 branch May 26, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

e2e-tested Test suite has run on this PR. Does not indicate tests pass/fail - see results in comments. test-e2e-smoke Triggers smoke e2e test suite [Currently SDK-only] tier1 verified Authorize secrets / label-gate in PR workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants