Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0b8399c
QVAC-24073 feat[bc]: adopt fabric b10297 consumers and replace no_mma…
donriddo Aug 26, 2026
a3efb02
Merge remote-tracking branch 'upstream/main' into feat/QVAC-24073-sdk…
donriddo Aug 26, 2026
896f404
QVAC-24073 fix: drop the stray streamx dependency and settle load-mod…
donriddo Aug 26, 2026
e1c1c4a
QVAC-24073 feat[bc]: reject unknown llamacpp load config keys on the …
donriddo Aug 27, 2026
c545e8b
QVAC-24073 test: format the llamacpp load-mode python tests with black
donriddo Aug 27, 2026
7f144b8
Merge remote-tracking branch 'upstream/main' into feat/QVAC-24073-sdk…
donriddo Aug 27, 2026
e1a03a0
QVAC-24073 fix: reject the retired no_mmap where the server actually …
donriddo Aug 27, 2026
aecddb0
QVAC-24073 fix: normalize a config-resolution rejection into a typed …
donriddo Aug 27, 2026
9f1bbb3
QVAC-24073 fix: make the llamacpp base config strict at its definition
donriddo Aug 27, 2026
76e5228
QVAC-24073 fix: reject a malformed modelConfig instead of coercing it…
donriddo Aug 27, 2026
587fada
QVAC-24073 test: pin load_mode to the addon union at compile time
donriddo Aug 27, 2026
5d47e0c
QVAC-24073 chore: drop the llm-llamacpp README from this SDK change set
donriddo Aug 27, 2026
ee039c6
QVAC-24073 chore: drop the website addon page from this SDK change set
donriddo Aug 27, 2026
66aea46
QVAC-24073 fix: pass ctx_size, not n_ctx, from the python and conform…
donriddo Aug 27, 2026
8702862
QVAC-24073 fix: drop the last n_ctx references
donriddo Aug 28, 2026
1e47b9a
Merge remote-tracking branch 'upstream/main' into feat/QVAC-24073-sdk…
donriddo Aug 28, 2026
eabd3c8
QVAC-24073 fix: narrow the strictness to the resolver and the wire sc…
donriddo Aug 28, 2026
f6e2f12
QVAC-24073 fix: drop the added strictness and everything it dragged in
donriddo Aug 28, 2026
f81b2c6
QVAC-24073 chore: restore the sdk dependency ordering
donriddo Aug 28, 2026
5221f23
QVAC-24073 chore: leave the conformance runner untouched
donriddo Aug 28, 2026
5b710f0
Merge branch 'main' into feat/QVAC-24073-sdk-load-mode
donriddo Aug 28, 2026
9f3eb14
Merge branch 'main' into feat/QVAC-24073-sdk-load-mode
donriddo Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions packages/inference/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,16 +192,16 @@
"@qvac/asr-ggml": "^0.3.0",
"@qvac/audiogen-ggml": "^0.2.1",
"@qvac/bci-whispercpp": "^0.7.1",
"@qvac/classification-ggml": "^0.20.0",
"@qvac/classification-ggml": "^0.22.0",
"@qvac/decoder-audio": "^0.5.0",
"@qvac/diffusion-cpp": "^0.20.0",
"@qvac/embed-llamacpp": "^0.34.0",
"@qvac/embed-llamacpp": "^0.36.0",
"@qvac/langdetect-text": "^0.1.2",
"@qvac/llm-llamacpp": "^0.45.0",
"@qvac/ocr-ggml": "^0.18.0",
"@qvac/translation-nmtcpp": "^0.10.0",
"@qvac/llm-llamacpp": "^0.47.0",
"@qvac/ocr-ggml": "^0.20.0",
"@qvac/translation-nmtcpp": "^0.12.0",
"@qvac/tts-ggml": "^0.7.4",
"@qvac/vla-ggml": "^0.21.1"
"@qvac/vla-ggml": "^0.23.0"
},
"peerDependenciesMeta": {
"@qvac/asr-ggml": {
Expand Down Expand Up @@ -248,16 +248,16 @@
"@qvac/asr-ggml": "^0.3.0",
"@qvac/audiogen-ggml": "^0.2.1",
"@qvac/bci-whispercpp": "^0.7.1",
"@qvac/classification-ggml": "^0.20.0",
"@qvac/classification-ggml": "^0.22.0",
"@qvac/decoder-audio": "^0.5.0",
"@qvac/diffusion-cpp": "^0.20.0",
"@qvac/embed-llamacpp": "^0.34.0",
"@qvac/embed-llamacpp": "^0.36.0",
"@qvac/langdetect-text": "^0.1.2",
"@qvac/llm-llamacpp": "^0.45.0",
"@qvac/ocr-ggml": "^0.18.0",
"@qvac/translation-nmtcpp": "^0.10.0",
"@qvac/llm-llamacpp": "^0.47.0",
"@qvac/ocr-ggml": "^0.20.0",
"@qvac/translation-nmtcpp": "^0.12.0",
"@qvac/tts-ggml": "^0.7.4",
"@qvac/vla-ggml": "^0.21.1",
"@qvac/vla-ggml": "^0.23.0",
"@types/brittle": "^3.5.0",
"bare": "*",
"bare-console": "*",
Expand Down
16 changes: 13 additions & 3 deletions packages/inference/src/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,19 @@ function applyDeviceDefaults<T extends Request>(request: T): T {
return request
}

const rawConfig = (request.modelConfig as Record<string, unknown>) ?? {}
const raw = request.modelConfig
// Spreading a non-object would coerce it into a valid defaults object; pass it through so the schema reports the type error.
if (raw !== undefined && raw !== null && !isPlainObject(raw)) return request

const rawConfig = (raw as Record<string, unknown>) ?? {}
return { ...request, modelConfig: resolveModelConfig(canonicalType, rawConfig) }
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
if (!value || typeof value !== 'object') return false
return !Array.isArray(value)
}

function getProfilingMeta(request: Request): ProfilingRequestMeta | undefined {
if (PROFILING_KEY in request) {
return (request as Record<string, unknown>)[PROFILING_KEY] as ProfilingRequestMeta
Expand All @@ -152,10 +161,11 @@ function getProfilingMeta(request: Request): ProfilingRequestMeta | undefined {
* parse would drop, so it is carried across.
*/
function prepareRequest<T extends Request>(request: T): Request {
const withDeviceDefaults = applyDeviceDefaults(request)
const profilingMeta = getProfilingMeta(withDeviceDefaults)
let validated: Request
let profilingMeta: ProfilingRequestMeta | undefined
try {
const withDeviceDefaults = applyDeviceDefaults(request)
profilingMeta = getProfilingMeta(withDeviceDefaults)
validated = requestSchema.parse(withDeviceDefaults)
} catch (error) {
if (error instanceof z.ZodError) {
Expand Down
9 changes: 7 additions & 2 deletions packages/inference/src/schemas/llamacpp-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const VERBOSITY = {
const verbositySchema = z.enum(VERBOSITY)

// Base schema - validates types, all fields optional (for input validation)
export const llmConfigBaseSchema = z.object({
export const llmConfigBaseSchema = z.strictObject({
Comment thread
donriddo marked this conversation as resolved.
Outdated
Comment thread
donriddo marked this conversation as resolved.
Outdated
ctx_size: z
.number()
.optional()
Expand Down Expand Up @@ -66,7 +66,12 @@ export const llmConfigBaseSchema = z.object({
.describe(
"Seeds conversation history on the JS side only; never forwarded to the addon. Default `'You are a helpful assistant.'`"
),
no_mmap: z.boolean().optional().describe('Disable memory-mapped model loading. Default false.'),
load_mode: z
.enum(['none', 'mmap', 'mlock', 'mmap+mlock', 'dio'])
.optional()
.describe(
"Model loading mode: `'none'`, `'mmap'`, `'mlock'`, `'mmap+mlock'`, or `'dio'`. Unset uses the addon's default (`'mmap'`)."
),
verbosity: verbositySchema
.optional()
.describe('Native log verbosity: `0`=ERROR, `1`=WARN, `2`=INFO, `3`=DEBUG. Default 0.'),
Expand Down
83 changes: 83 additions & 0 deletions packages/inference/test/config-resolution-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
BUILTIN_DEVICE_PATTERNS,
type ConfigResolutionLog
} from '@/runtime/model-config-utils'
import { validateConfig } from '@/config/config-utils'

test('matchesPattern: matches Pixel device', (t) => {
const ctx: RuntimeContext = {
Expand Down Expand Up @@ -346,3 +347,85 @@ test('BUILTIN: User can override device defaults', (t) => {

t.is(result.device, 'vulkan')
})

// dispatch.ts resolves config here, before the wire schema, so this is the layer that decides.

test('RESOLVER: retired no_mmap is rejected, not silently stripped', (t) => {
const ctx: RuntimeContext = { platform: 'linux' }

t.exception(
() =>
resolveModelConfigWithContext<Record<string, unknown>>(
ModelType.llamacppCompletion,
{ ctx_size: 2048, no_mmap: true },
ctx,
[],
BUILTIN_DEVICE_PATTERNS
),
/no_mmap/
)
})

test('RESOLVER: every load_mode survives resolution', (t) => {
const ctx: RuntimeContext = { platform: 'linux' }

for (const load_mode of ['none', 'mmap', 'mlock', 'mmap+mlock', 'dio']) {
const result = resolveModelConfigWithContext<Record<string, unknown>>(
ModelType.llamacppCompletion,
{ load_mode },
ctx,
[],
BUILTIN_DEVICE_PATTERNS
)

t.is(result.load_mode, load_mode)
}
})

test('RESOLVER: an unknown load_mode is rejected', (t) => {
const ctx: RuntimeContext = { platform: 'linux' }

t.exception(
() =>
resolveModelConfigWithContext<Record<string, unknown>>(
ModelType.llamacppCompletion,
{ load_mode: 'buffered' },
ctx,
[],
BUILTIN_DEVICE_PATTERNS
),
/load_mode/
)
})

test('RESOLVER: omitted load_mode stays omitted', (t) => {
const ctx: RuntimeContext = { platform: 'linux' }

const result = resolveModelConfigWithContext<Record<string, unknown>>(
ModelType.llamacppCompletion,
{ ctx_size: 2048 },
ctx,
[],
BUILTIN_DEVICE_PATTERNS
)

t.is('load_mode' in result, false)
})

function devicePatternConfig(key: string, modelConfig: Record<string, unknown>) {
return {
deviceDefaults: [{ name: 't', match: { platform: 'linux' }, defaults: { [key]: modelConfig } }]
}
}

for (const key of ['llm', 'llamacpp-completion']) {
test(`CONFIG: deviceDefaults.${key} rejects the retired no_mmap`, (t) => {
t.exception(() => validateConfig(devicePatternConfig(key, { no_mmap: true })), /no_mmap/)
})

test(`CONFIG: deviceDefaults.${key} keeps a valid load_mode`, (t) => {
const config = validateConfig(devicePatternConfig(key, { load_mode: 'none' }))
const defaults = config.deviceDefaults?.[0]?.defaults as Record<string, Record<string, unknown>>
t.is(defaults[key].load_mode, 'none')
})
}
48 changes: 48 additions & 0 deletions packages/inference/test/dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,51 @@ test('close resets readiness so the next call re-runs the guard', async function
await t.exception(() => send(fakeRequest('heartbeat')), PluginsNotRegisteredError)
await close()
})

test('send normalizes a config-resolution rejection into a typed error', async function (t) {
clearPlugins()
registerPlugin(makeFakePlugin(ModelType.llamacppCompletion))
try {
await send({
type: 'loadModel',
modelSrc: '/m.gguf',
modelType: ModelType.llamacppCompletion,
modelConfig: { no_mmap: true }
} as never)
t.fail('expected send to reject the retired no_mmap key')
} catch (error) {
t.ok(
error instanceof RequestValidationFailedError,
'a resolver rejection is normalized, not surfaced as a raw ZodError'
)
} finally {
await close()
clearPlugins()
}
})

test('send rejects a malformed modelConfig instead of coercing it to defaults', async function (t) {
clearPlugins()
registerPlugin(makeFakePlugin(ModelType.llamacppCompletion))
try {
for (const malformed of [false, 42, 'abc', [], ['a']]) {
try {
await send({
type: 'loadModel',
modelSrc: '/m.gguf',
modelType: ModelType.llamacppCompletion,
modelConfig: malformed
} as never)
t.fail(`expected rejection for modelConfig=${JSON.stringify(malformed)}`)
} catch (error) {
t.ok(
error instanceof RequestValidationFailedError,
`modelConfig=${JSON.stringify(malformed)} is a type error, not silent defaults`
)
}
}
} finally {
await close()
clearPlugins()
}
})
39 changes: 39 additions & 0 deletions packages/inference/test/llm-config-schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import test from 'brittle'
import type LlmLlamacpp from '@qvac/llm-llamacpp'
import type { z } from 'zod'
import {
llmConfigBaseSchema,
llmConfigSchema,
Expand Down Expand Up @@ -26,6 +28,28 @@ test('llmConfigBaseSchema: split-mode is optional', (t) => {
t.is(llmConfigBaseSchema.safeParse({}).success, true)
})

test('llmConfigBaseSchema: accepts every load_mode value', (t) => {
for (const load_mode of ['none', 'mmap', 'mlock', 'mmap+mlock', 'dio'] as const) {
const result = llmConfigBaseSchema.safeParse({ load_mode })
t.is(result.success, true, `${load_mode} must be accepted`)
if (result.success) t.is(result.data.load_mode, load_mode)
}
})

test('llmConfigBaseSchema: rejects invalid load_mode values', (t) => {
t.is(llmConfigBaseSchema.safeParse({ load_mode: 'buffered' }).success, false)
})

test('llmConfigBaseSchema: rejects legacy no_mmap under strict validation', (t) => {
t.is(llmConfigBaseSchema.strict().safeParse({ no_mmap: true }).success, false)
})

test('llmConfigSchema: leaves load_mode unset by default', (t) => {
const result = llmConfigSchema.safeParse({})
t.is(result.success, true)
if (result.success) t.is(result.data.load_mode, undefined)
})

test('llmConfigBaseSchema: accepts continuous-batching parallel slots', (t) => {
const result = llmConfigBaseSchema.safeParse({ parallel: 4 })
t.is(result.success, true)
Expand Down Expand Up @@ -213,3 +237,18 @@ test('loadModelOptionsToRequestSchema: accepts mmproj-use-gpu for LLM', (t) => {
true
)
})

// Fails to compile if the addon widens or narrows its own load_mode union.
type AddonLoadMode = NonNullable<LlmLlamacpp.LlamaConfig['load_mode']>
type SdkLoadMode = NonNullable<z.infer<typeof llmConfigBaseSchema>['load_mode']>
type ExactlyEqual<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false
const loadModeMatchesAddon: ExactlyEqual<AddonLoadMode, SdkLoadMode> = true

test('llmConfigBaseSchema: load_mode matches the addon union exactly', (t) => {
t.ok(loadModeMatchesAddon)
})

test('llmConfigBaseSchema: rejects the retired no_mmap without a test-applied strict()', (t) => {
t.is(llmConfigBaseSchema.safeParse({ no_mmap: true }).success, false)
t.is(llmConfigBaseSchema.safeParse({ load_mode: 'none' }).success, true)
})
7 changes: 7 additions & 0 deletions packages/inference/test/llm-plugin-transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ test('transformLlmConfig: positive reasoning_budget survives as string token cap
)
})

test('transformLlmConfig: load_mode survives as an underscore key', (t) => {
const config = makeConfig({ load_mode: 'mmap+mlock' })
const result = transformLlmConfig(config)
t.is(result['load_mode'], 'mmap+mlock')
t.absent('load-mode' in result)
})

test('transformLlmConfig: stop_sequences is renamed to reverse_prompt', (t) => {
const config = makeConfig({ stop_sequences: ['</s>', '<|im_end|>'] })
const result = transformLlmConfig(config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@
LoadModelSrcRequestLlamacppCompletionModelConfig,
LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale,
LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode,
LoadModelSrcRequestLlamacppCompletionModelConfigLoadMode,
LoadModelSrcRequestLlamacppCompletionModelConfigMainGpu,
LoadModelSrcRequestLlamacppCompletionModelConfigProjectionModelSrc,
LoadModelSrcRequestLlamacppCompletionModelConfigProjectionModelSrcAddon,
Expand Down Expand Up @@ -1260,6 +1261,7 @@
"LoadModelSrcRequestLlamacppCompletionModelConfig",
"LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale",
"LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode",
"LoadModelSrcRequestLlamacppCompletionModelConfigLoadMode",
"LoadModelSrcRequestLlamacppCompletionModelConfigMainGpu",
"LoadModelSrcRequestLlamacppCompletionModelConfigProjectionModelSrc",
"LoadModelSrcRequestLlamacppCompletionModelConfigProjectionModelSrcAddon",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6465,6 +6465,14 @@ class Predict(RootModel[int]):
]


class LoadModelSrcRequestLlamacppCompletionModelConfigLoadMode(Enum):
none = "none"
mmap = "mmap"
mlock = "mlock"
mmap_mlock = "mmap+mlock"
dio = "dio"


class LoadModelSrcRequestLlamacppCompletionModelConfigVerbosity(Enum):
number_0 = 0
number_1 = 1
Expand Down Expand Up @@ -6554,6 +6562,9 @@ class LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale(Enum):


class LoadModelSrcRequestLlamacppCompletionModelConfig(GeneratedBaseModel):
model_config = ConfigDict(
extra="forbid",
)
ctx_size: Annotated[
float | None,
Field(
Expand Down Expand Up @@ -6614,9 +6625,12 @@ class LoadModelSrcRequestLlamacppCompletionModelConfig(GeneratedBaseModel):
description="Seeds conversation history on the JS side only; never forwarded to the addon. Default `'You are a helpful assistant.'`"
),
] = None
no_mmap: Annotated[
bool | None,
Field(description="Disable memory-mapped model loading. Default false."),
load_mode: Annotated[
LoadModelSrcRequestLlamacppCompletionModelConfigLoadMode | None,
Field(
description="Model loading mode: `'none'`, `'mmap'`, `'mlock'`, `'mmap+mlock'`, or `'dio'`. Unset uses the addon's default (`'mmap'`).",
title="LoadModelSrcRequestLlamacppCompletionModelConfigLoadMode",
),
] = None
verbosity: Annotated[
LoadModelSrcRequestLlamacppCompletionModelConfigVerbosity | None,
Expand Down
Loading
Loading