Skip to content

QVAC-24251 feat[bc|api]: drop n_discarded from the SDK config schema - #4163

Merged
iancris merged 61 commits into
mainfrom
feat/QVAC-24251-sdk-drop-n-discarded
Sep 2, 2026
Merged

iancris merged 61 commits into
mainfrom
feat/QVAC-24251-sdk-drop-n-discarded

Conversation

@donriddo

@donriddo donriddo commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

🎯 What problem does this PR solve?

  • The llm-llamacpp addon stops consuming n_discarded in QVAC-23752 feat[bc]: remove sliding-context support from the llm-addon #3938, so the key reaches llama's own argument parser and fails model load as an unknown option. Every in-repo consumer still sends it.
  • parseContextOverflowMessage matched two of the addon's numeric overflow wordings. The two warm-cache guards and the multimodal single-prompt wording parsed as nothing, so the typed error crossed the RPC with no sizes — and the multimodal gap is live against the published 0.45.0/0.47.0, not only against QVAC-23752 feat[bc]: remove sliding-context support from the llm-addon #3938's new wordings.
  • isAddonContextOverflowError's message-only fallback did not know the at batch prefill step wording. The async transport strips status metadata from every run error, so in production the message alone identifies an overflow; the fallback now covers every emitted wording — the model guards' tagged forms and the batch scheduler's per-sequence-cap refusals, which are the same out-of-context condition and previously surfaced untyped at parallel >= 2 — start-anchored so wrapper or cause-chain text cannot select cache preservation, and a present status code is authoritative — exact ContextOverflow accepts, any other code rejects without a message fallback. (Typing the scheduler refusals by wording is the interim fix; giving them a real ContextOverflow status addon-side is a recorded follow-up.)
  • The parser exposed cached totals — and, on multimodal guards, KV-cell counts — under the token-named public field promptTokens, so a caller's truncation logic could overestimate the current prompt by the entire cached conversation, and multimodal callers received cells under a token name. The error message had the same problem for the retired short form (it blamed a "prompt" of the cached-total size), and ctxSize was documented as the configured ctx_size when the addon actually reports the effective per-sequence ceiling (ctx_size / parallel under continuous batching).
  • With context no longer evicted, the two ways a caller runs out of context had no consumer-level coverage: a generation that fills the window, a prefill that never fits, and a warm cache grown past the window.

This re-does #3999 on top of the inference/sdk package split; that PR edits packages/sdk server files the split has since deleted, so its runtime changes are re-homed here into the canonical packages/inference sources. The parser, schema and doc work is @yingying0906's.

📝 How does it solve it?

  • n_discarded removed from the zod schema, so loadModel() rejects it at validation, and contract/schema.json plus the sdk-python client regenerated from it with the pinned generators.
  • n_discarded is deleted from the config schema, following the retired-key precedent (toolsMode QVAC-22567 feat[bc]: remove SDK dynamic tools mode (toolsMode) #3380, no_mmap QVAC-24073 feat[bc]: adopt fabric b10297 consumers and replace no_mmap with load_mode #4078): the long-standing strict loadModel() options schema rejects it for TypeScript/JS callers, and the other ingestion paths strip unknown keys as they always have. No schema gained strictness or changed shape beyond the field removal (an earlier revision strictified the base and reworked the request union; both were reverted to match the recorded QVAC-24073 decision).
  • Removed from every e2e config that passed n_discarded: 256.
  • toolBlockEvictable is gone. It existed because the addon's discard window opened exactly where the static tool block sat, so while sliding was possible the block had to travel with every turn. Nothing evicts it now, so a warm turn skips it whenever the prefix is known to hold a rendered one.
  • Every overflow pattern maps its captures to typed fields with honest units. promptTokens carries only figures the guard denominates in tokens — never a cached total, never KV cells. Two new optional fields on ContextOverflowError carry the rest: cachedTokens (the cached conversation a warm-cache guard reports) and requiredTokens (the total that failed the guard), both in the same units as ctxSize — KV cells, which ctx_size counts, so the pair stays comparable; cells equal tokens for text. The multimodal guards trip on EITHER positions or KV cells against the same ceiling; valid addon state keeps cells >= positions, so capturing the larger measure is defensive parsing (pinned by labelled malformed-input probes), and requiredTokens >= ctxSize holds on every reachable guard. The guards trigger on >= for a request that must still generate, so requiredTokens may equal the window; the tests assert the >= floor, not >. The fields serialize through toErrorResponseFields, rebuild in the SDK client's RPC reconstructor and the Python reconstructor, and the error message names both halves on a warm cache instead of blaming the appended prompt alone.
  • Three consumer-level tests are registered on the desktop, electron and mobile consumers against a dedicated 512-token llm resource (same model constant as llm, no new download): a generation that fills the window must surface as the public stopReason: "length" with its produced tokens retained and generatedTokens below the 1000 predict budget (far above the whole window), proving the boundary rather than budget exhaustion fired (the plugin maps the addon's context-boundary stop to "length", there is no public contextOverflow stop reason); an oversized cold prefill must reject with the typed ContextOverflowError whose sizes reach the window and whose ctxSize equals the configured 512; and a real warm cache grown past the window must reject the follow-up with the warm signature — a cachedTokens floor well above what a fresh system-prompt prime can hold, so a silently rolled-back first turn cannot pass, after asserting the first turn ended commit-eligible — with the per-run cache deleted in cleanup and a failed deletion failing the test rather than being swallowed — and the same follow-up retried before cleanup must overflow warm again, pinning that the rejection did not destroy the committed cache.
  • A refused request used to run the turn's unconditional rollback, unlinking the last committed cache file the addon had deliberately left intact — so an oversized follow-up cost the whole warm conversation. The session gains releaseTurn, a non-destructive counterpart used when the failure is a thrown addon ContextOverflow, one of the addon's pre-mutation InvalidArgument refusals (the scheduler and generationParams guards, and the multimodal media-load failures), or an AttachmentNotFoundError (caller input rejected by the SDK before the addon runs) — the scheduler's per-sequence-cap and batcher AddStatus submit rejections, and the generationParams apply-step validation (reachable at parallel = 1 via responseFormat) — recognised by complete, end-anchored wordings (one per emitting guard; batcher AddStatus restricted to its real error values). The async transport delivers the addon's exception.what() alone — no status code — so the wording is the boundary; a status code is only checked when present, and must be InvalidArgument. Locks, refs and the deferred auto-cache retention sweep behave as on rollback; the disk cache and its recorded prefix survive. A cache the failing turn itself primed rolls back instead — a refused first request leaves no cache behind. Preservation is deliberately limited to these recognised cases — other pre-persistence failures (a media prompt on a text-only model, an empty history, an atomic-save UnableToSaveSessionFile failure that leaves the previous canonical file valid) still take the destructive default and are recorded follow-ups, pending a machine-readable addon status. Unit regressions pin the warm retry for the refusal shapes, throwing the production plain-Error shape the transport actually delivers (each fails with its branch reverted), and a session-level test pins that release preserves the file/prefix and admits a same-key waiter.
  • The KV-cache guide told readers to add n_discarded for long conversations, named it as the fix for a prefill overflow, and promised a stopReason: "contextOverflow" the public SDK never returns. It now describes the two ways a caller runs out of context, says the boundary surfaces as "length" and that stopReason alone does not distinguish it from a positive predict cutoff, and that loadModel() rejects the retired key at validation.

Adoption. #3938 released as 0.48.0 while this PR was in review, so the @qvac/llm-llamacpp pins in inference and sdk move to ^0.48.0 here, the same shape as the load_mode adoption in #4078. Parser compatibility with a 0.47.x worker is pinned by exact-string unit tests (the retired wordings stay as patterns); the e2e suite in its current, strengthened form is proven against 0.48.0 — the warm-cache test requires the rich cachedTokens signature that 0.47.x's short form does not emit. (Earlier, weaker versions of these e2e tests also passed against 0.47.0 before the bump.)

🧪 How was it tested?

  • packages/inference: lint clean, full unit suite green (1603+ tests). That includes one case per addon guard in the context-overflow parser suite using the exact emitted strings with per-guard field expectations, the pre-existing 0.47.x wordings kept as regression cases, a case asserting requiredTokens reaches the window (>=, the floor the guards trigger on), the flipped KV-cache regression (a config still carrying the retired key no longer forces a tool-block resend), the schema-rejection tests for the options path, and the typed-fields round-trip for the new error fields.
  • packages/sdk: lint, typecheck, unit tests and contract:check all clean.
  • packages/sdk-python: generate.py --check clean, pytest suite passes, including the reconstructor carrying cached_tokens / required_tokens.
  • Verify-gated full run (at faf569b87; the later commits are unit-covered refinements on top): the three overflow tests passed on macOS, Linux, Windows, Android and iOS (iOS leg fully green). The only per-leg failures are outside this PR — an identical rag-turbovec-ingest-search miss on all three desktops and one Android cancel-broad-embeddings. Electron/Snap were skipped by that workflow, so Electron still lacks execution evidence (locally blocked by the harness --install-links issue). Local desktop e2e, completion (50/50) and kv-cache (21/21) categories against the pinned 0.48.0, covers completion-context-boundary-stop (stopReason length at the boundary, produced tokens retained), completion-context-overflow-prefill (typed ContextOverflowError, sizes reach the window, ctxSize equals the configured 512), completion-context-overflow-warm-cache (a real cached first turn, follow-up refused with the failing total, cache deleted in cleanup), and the tool-block skip/resend paths the toolBlockEvictable removal touches.

💥 Breaking Changes

n_discarded is removed from the llamacpp model config schema. A TypeScript/JS loadModel() call passing it fails validation (the options schema has been strict since long before this change); raw wire requests, deviceDefaults, and the generated Python client strip it like any unknown key — the same treatment toolsMode (#3380) and no_mmap (#4078) received. The behavioural change is the addon's: on 0.48 a conversation that previously slid its window now stops at the context boundary (stopReason: "length") or is refused up front with a typed ContextOverflowError. (Release note: belongs in the CHANGELOG at the release cut.)

BEFORE:

await loadModel({
  modelSrc: MODEL,
  modelType: 'llm',
  modelConfig: { ctx_size: 2048, n_discarded: 256 }
})

AFTER:

await loadModel({
  modelSrc: MODEL,
  modelType: 'llm',
  modelConfig: { ctx_size: 2048 }
})

🔌 API Changes

ContextOverflowError gains two optional fields, in TypeScript and in the Python client (cached_tokens / required_tokens). promptTokens now carries only figures known to be the prompt alone, in tokens — on a warm-cache overflow it is the appended prompt, not the cached total; multimodal guards that report KV cells leave it unset; and the published 0.47.x short form (N tokens, max M) leaves it unset too, since both of its emitters format a cached total.

try {
  await completion({ modelId, history, kvCache }).final
} catch (err) {
  if (err instanceof ContextOverflowError) {
    err.requiredTokens // total context the request needs, in ctxSize units
    err.cachedTokens // cached conversation, on a warm-cache overflow
    err.promptTokens // the prompt alone, only when reported in tokens
    err.ctxSize // effective ceiling for this request: ctx_size / parallel slots
  }
}

The canonical constructor takes the four measurements as one record — new ContextOverflowError(contextSizes, modelId, cause) — with the positional form kept as a deprecated overload for existing callers.

The error message is unit-neutral where the value can be KV cells — "Conversation uses 8201 context units (8170 already cached) and leaves no room to generate within the effective context capacity (8192 units)" — phrased around the free output slot so it also reads coherently at the >= equality boundary, and it never blames a "prompt" of the cached-total size.

The llm addon no longer consumes n_discarded (#3938), so the
key would reach llama's own argument parser and fail model load as an
unknown option. Remove it from the zod schema so loadModel() rejects it at
validation, regenerate the exported contract and the Python client, and
drop it from every e2e model config. The KV-cache guide now describes the
two overflow surfaces instead of recommending sliding.
parseContextOverflowMessage matched two of the addon's numeric overflow
wordings. The two warm-cache guards and the multimodal single-prompt
wording parsed as nothing, so the typed error carried no sizes — the
multimodal gap is live against the published 0.47.x, not only against
the new wordings. Every pattern now captures the context size last and
sums the leading groups, KV cells are preferred where a guard reports
positions too, and isAddonContextOverflowError recognises the 'at batch
prefill step' wording on the message-only fallback. Sizes are validated
as finite, and the tests assert the >= floor the guards actually trigger
on, since the failing requirement can equal the window.
…kv-cache path

toolBlockEvictable existed because the addon's discard window opened
exactly where the static tool block sat, so while sliding was possible
the block had to travel with every turn. Nothing evicts it any more, so
a warm turn skips it whenever the prefix is known to hold a rendered one.
The regression test now pins that a config still carrying the retired key
does not force a resend.
…he e2e consumers

A dedicated 512-token llm resource (same constant as llm, no new
download) backs two completion tests on desktop, electron and mobile: a
generation that fills the window must surface as the public stopReason
'length' with its produced tokens retained (predict is -1 and the prompt
has no natural terminus, so the boundary is the only length source), and
an oversized prefill must reject with the typed ContextOverflowError
whose parsed sizes reach the window.
@donriddo
donriddo requested review from a team as code owners August 31, 2026 12:58
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Review Status

Current Status: ✅ APPROVED
Approvals so far: Team Lead: 1, Member: 1

@github-actions

Copy link
Copy Markdown
Contributor

License compliance — clean

No 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):

  • ./.github/actions/release-merge-guard
  • ./docs/website
  • ./packages/ggml-coload-smoke
  • ./packages/fabric/test/integration
  • ./packages/inference-addon-cpp/mobile
  • ./packages/sdk/e2e
  • ./packages/llm-llamacpp/benchmarks/performance
  • ./packages/llm-llamacpp/benchmarks/server
  • ./packages/vla-ggml/sim/server
  • ./packages/embed-llamacpp/benchmarks/performance
  • ./packages/embed-llamacpp/benchmarks/server
  • ./packages/asr-ggml/benchmarks/server

…stion path

Only the TypeScript options schema was strict, so the raw wire request,
the plugin's loadConfigSchema and both deviceDefaults entries stripped
the retired key and loaded with sliding silently off. All four now use
llmConfigBaseSchema.strict(). The contract's LLM modelConfig gains
additionalProperties: false, which the Python generator turns into
extra="forbid", so a Python caller constructing a config with
n_discarded gets a ValidationError instead of a silent drop. Rejection
tests pin the wire schema, both deviceDefaults keys and the generated
Python model.
…rflowError

The parser summed cached totals — and, on multimodal guards, KV-cell
counts — into the token-named promptTokens, so truncation logic could
overestimate the prompt by the whole cached conversation and multimodal
callers received cells under a token name. promptTokens now carries only
figures the guard denominates in tokens; two new optional fields,
cachedTokens and requiredTokens, carry the cached conversation and the
total that failed the guard, in the same units as ctxSize. The fields
serialize through toErrorResponseFields, rebuild in the SDK RPC
reconstructor and the Python reconstructor, and the error message names
both halves on a warm cache.
The guide promised stopReason "contextOverflow", which the public SDK
never returns: the plugin maps the addon's context-boundary stop and a
positive predict cutoff both to "length". Say so, and that stopReason
alone does not distinguish the two.
The parser's most substantial additions are the cached-plus-prompt
guards, and nothing public exercised them. A third consumer test caches
a first turn that fills most of the 512 window under a per-run key, then
sends a follow-up that fits the window alone but not on top of the
cache, and asserts the typed error carries a requiredTokens no smaller
than the window; the cache is deleted in cleanup. The cold-prefill test
now also pins ctxSize to the configured 512, and the boundary-stop
comment no longer claims an early EOS is impossible.
@donriddo donriddo changed the title QVAC-24251 feat[bc]: drop n_discarded from the SDK config schema QVAC-24251 feat[bc|api]: drop n_discarded from the SDK config schema Aug 31, 2026
…w form

Both emitters of "(N tokens, max M)" format a cached total (text:
nPast_ + nTokens, multimodal: cacheTokens + nTokens), so on a warm cache
the figure is not the prompt alone. The short form now maps to
requiredTokens only, matching the field's prompt-only contract, and the
short-form test pins the unset field. Comments trimmed to a line or two
and version strings dropped from source.
The addon release carrying the sliding-context removal. Caret on 0.x is
patch-only, so the pins in inference (dependency and peer) and sdk move
to ^0.48.0 explicitly. The overflow parsers keep the older wordings, so
a worker still on 0.47.x keeps parsing.
…quest schema

Dispatch applies device defaults before the request schema runs, and the
default-applying parse was non-strict, so it stripped n_discarded ahead
of every strict validation added so far. llmConfigBaseSchema is now
strict at the source, so that first parse and every derivation — wire,
deviceDefaults, plugin loadConfigSchema, the public export — reject the
key, and dispatch wraps the defaults parse so the failure surfaces as a
structured RequestValidationFailedError. Regressions pin send() and the
config-resolution path.
… structurally

The catch-all's built-in exclusion was a zod refine, which does not
serialize, so the exported contract and the generated Python union
accepted a built-in modelType with arbitrary config through the
permissive arm. A regex over the canonical types and aliases carries the
rule into both. Closing the leak exposed the wire arms it had been
masking: they required modelConfig even though the server injects
defaults, so their optionality now mirrors the options schemas (llm,
whisper, bci, embeddings, ocr optional; nmt, tts, audiogen required),
and the transport tests that validated enum-typed requests through the
leak now send the wire strings. Python tests pin the union, the public
load_model() (rejects before the transport; custom types still pass) and
the generated model.
…Size docs

A lone requiredTokens can be a cold multimodal prompt in KV cells or the
retired short form's cached total in tokens, so the message no longer
blames a 'prompt spanning N KV cells' — it says what is known: the
request needs N context tokens and no longer fits. ctxSize is documented
as the effective per-request ceiling (ctx_size split across slots at
parallel > 1), not the configured total, in the parser, both error
classes, the Python docstring and the KV-cache guide. The parser's
separators are horizontal whitespace only, matching its single-line
claim. Message-level regressions pin the warm and required-only shapes.
…p failures

The warm-cache e2e now asserts the first turn ended commit-eligible (no
stop reason, non-empty output) and that the error carries a positive
cachedTokens, so a rolled-back first turn or a cold full-history resend
fails instead of passing through the plain prefill guard. A failed cache
deletion fails the test rather than leaking a named cache. The boundary
test replaces predict -1 with a 480 budget above the window's remaining
capacity and asserts generatedTokens lands under it, so a 'length' stop
provably means the boundary. The JS RPC round-trip test pins
cachedTokens and requiredTokens across the envelope.
Comment blocks across the diff shrink to one or two lines each, and the
tests/test_load_model.py import moves to its alphabetical position,
which the Ruff import-order check requires.
…uality

A lone or cached total can be KV cells, so the message says 'context
units', and the guards trigger at equality — a generating request needs
a free slot — so it is phrased as leaving no room to generate rather
than exceeding, which read as a contradiction when the total equalled
the capacity. The Python default message follows suit, and the stale
class docs that still described a prompt against a configured window now
describe the optional fields. Regressions pin the unit-neutral and
equality wordings.
…rm assertions

The custom-plugin regression dereferenced .root.root on a union arm
mypy cannot narrow from the input dict, failing the required Python
typecheck; an isinstance assertion narrows it. The dispatch regression
now asserts the structured RequestValidationFailedError class, not just
the message. The warm-cache executor deletes the per-run cache even when
the flow throws before its inner handling.
The MtmdLlm guards trip on EITHER positions or KV cells against the same
ceiling, but the mappings captured only one side, so a positions-dominant
overflow reported a figure below the window — contradicting the
requiredTokens >= ctxSize contract the fields document. Both figures are
captured now and the larger is what failed the guard. Positions-dominant
regressions pin all three multimodal wordings.
The enum's members are not str, so they only ever validated through the
custom-plugin catch-all; with that arm excluding built-ins the public
load_model() started rejecting them. The signature accepts the enum and
coerces it to its wire string, with a regression pinning it.
The boundary test's 480 budget sat ~2% from the window's usable capacity,
so a generous tokenizer could stop on the prediction cutoff and fail the
boundary proof; 1000 is unambiguous. The warm test's first turn gets 48
tokens so a short ramble still ends on EOS and stays commit-eligible.
…on regex

The base schema is strict, so the six per-site strict() wrappers are
no-ops. A guard test pins that every built-in type and alias stays a
plain kebab identifier, since the custom-plugin exclusion regex
interpolates them unescaped.
…fault message

A commit rollback returns normally and turn two re-primes the cache, so
a positive cachedTokens alone does not prove the first turn survived —
the assertion now requires a floor well above what a system-prompt prime
can hold. The Python direct-construction default said 'exceeds', which
is false at the equality boundary the guards trigger on; it now matches
the TS wording's neutrality, with direct-construction regressions for
the equality and warm shapes.
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

QVAC E2E — ios⚠️ no results

Config: suite=(none) · filter=(none) · exclude=(none)
Inference: branch:fbe5d493609c3c46df47cba94b448670605cb16e:@qvac/inference@0.18.2 · Test-suite: branch:fbe5d493609c3c46df47cba94b448670605cb16e:@qvac/test-suite@0.11.0
Device pool: iPhone 17 Pro - iOS 26 — ❔ unknown
View run

The test job did not produce a results artifact (e.g. no device started within the start-timeout, or a job-level failure). Check the run and the device pool status above.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

QVAC E2E — windows — ❌ failed

Totals: 508/512 passed · 1 failed · 99.8% · 2332s
Config: suite=(none) · filter=(none) · exclude=(none)
Inference: branch:fbe5d493609c3c46df47cba94b448670605cb16e:@qvac/inference@0.18.2 · Test-suite: branch:fbe5d493609c3c46df47cba94b448670605cb16e:@qvac/test-suite@0.11.0
View run · Artifacts: reports

Results by section

  • rag: 9/10 ❌

Failed tests

  • rag-turbovec-ingest-search: Missing required strings: checkpoint:present. Got: the verification code is orange-742.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

QVAC E2E — android⚠️ no results

Config: suite=(none) · filter=(none) · exclude=(none)
Inference: branch:fbe5d493609c3c46df47cba94b448670605cb16e:@qvac/inference@0.18.2 · Test-suite: branch:fbe5d493609c3c46df47cba94b448670605cb16e:@qvac/test-suite@0.11.0
Device pool: Samsung S26 Ultra - Android 16 — ❔ unknown
View run

The test job did not produce a results artifact (e.g. no device started within the start-timeout, or a job-level failure). Check the run and the device pool status above.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

QVAC E2E — linux — ❌ failed

Totals: 505/512 passed · 4 failed · 99.2% · 1967s
Config: suite=(none) · filter=(none) · exclude=(none)
Inference: branch:fbe5d493609c3c46df47cba94b448670605cb16e:@qvac/inference@0.18.2 · Test-suite: branch:fbe5d493609c3c46df47cba94b448670605cb16e:@qvac/test-suite@0.11.0
View run · Artifacts: reports

Results by section

  • rag: 9/10 ❌
  • world: 5/8 ❌

Failed tests

  • rag-turbovec-ingest-search: Missing required strings: checkpoint:present. Got: the verification code is orange-742.
  • world-create-scene-returns-pack: Handler execution failed: Bare worker exited mid-request (code=null, signal=SIGABRT) — in-flight calls were aborted
  • world-first-block-frames: Handler execution failed: Model with ID "11b875ec8eda93a9" not found
  • world-second-block-frames: Handler execution failed: Model with ID "11b875ec8eda93a9" not found

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

QVAC E2E — macos — ❌ failed

Totals: 505/512 passed · 4 failed · 99.2% · 1286s
Config: suite=(none) · filter=(none) · exclude=(none)
Inference: branch:fbe5d493609c3c46df47cba94b448670605cb16e:@qvac/inference@0.18.2 · Test-suite: branch:fbe5d493609c3c46df47cba94b448670605cb16e:@qvac/test-suite@0.11.0
View run · Artifacts: reports

Results by section

  • rag: 9/10 ❌
  • world: 5/8 ❌

Failed tests

  • rag-turbovec-ingest-search: Missing required strings: checkpoint:present. Got: the verification code is orange-742.
  • world-create-scene-returns-pack: Handler execution failed: Bare worker exited mid-request (code=null, signal=SIGABRT) — in-flight calls were aborted
  • world-first-block-frames: Handler execution failed: Model with ID "11b875ec8eda93a9" not found
  • world-second-block-frames: Handler execution failed: Model with ID "11b875ec8eda93a9" not found

@yingying0906 yingying0906 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.

Reviewed at fbe5d4936. Two things I think need a change before merge, both in ops/context-overflow.ts, plus three notes on files this diff doesn't touch.

1. Scheduler cap overflows never become a typed error. The four per-sequence cap forms are in PRE_MUTATION_REFUSAL_FORMS but not in CONTEXT_OVERFLOW_FORMS, so with parallel > 1 the same "no room to generate" condition gives an untyped error. Same conversation at parallel: 1 gives ContextOverflowError. Details inline.

2. Addon media-load failures unlink a valid cache. The three loadMedia throws are pre-decode and pre-save, but they're not in the refusal list, so session.rollback deletes a committed cache file that was never touched. You already handle the SDK-side twin AttachmentNotFoundError, so this looks like an oversight. Details inline.

Three notes I can't anchor to a diff line:

rpc-error.ts:181 reads RECONSTRUCTORS[response.name] off an object literal, so the lookup walks Object.prototype. A payload with name: "constructor" resolves to the Object constructor, passes the truthiness check at :182, and Object(response) returns the response itself, so reconstructError returns a non-Error while the signature says Error. The other inherited keys throw and get caught at :186-198, so constructor is the only one that gets through. It's a pre-existing line, but you're changing the reconstructor it dispatches to. Fix is Object.create(null) or a hasOwnProperty gate. A typeof === 'function' check won't do it since Object is a function.

load-model.ts:157 and :286 wrap the base schema as llmConfigBaseSchema.strict(), so n_discarded now throws on the options path, while the wire schema at :566 and the Python client strip it. I can see from the test comment at llm-config-schema.test.ts:59-60 that the split is deliberate. Can it go in the PR body too? A JS caller gets a load-time throw and a Python caller gets silence for the same input, and right now that only lives in a test comment.

packages/rag/package.json:77 is still @qvac/llm-llamacpp: ^0.47.0 while inference and sdk went to ^0.48.0. It's a devDependency and rag has no source use of the addon, so nothing ships wrong, but a 0.x caret won't cross the minor so rag's tests run against the addon that still consumes n_discarded.

Rest of the parse layer checks out. I diffed all six ContextOverflow throw sites against the regexes character by character and the wordings, capture indices and units all match, including the exceeds? singular/plural split and ctxCeiling() matching the ctxSize doc. Detection is also harder to spoof than the old substring fallback.

Comment thread packages/inference/test/context-overflow.test.ts
Comment thread packages/inference/src/schemas/errors.ts
Comment thread packages/inference/src/errors/index.ts Outdated
…erve cache on media-load failures

The scheduler's per-sequence-cap refusals are the same out-of-context
condition the model guards report, so their wordings join the overflow
forms and the parser maps cap to ctxSize (reservation plus prompt on the
n_predict form) — previously the identical conversation was typed at
parallel 1 and untyped above it. Interim wording-based fix; the addon
carrying a real ContextOverflow status stays the recorded follow-up.
The multimodal media-load failures join the pre-mutation refusals: they
reject before any decode or save, like the SDK-side missing attachment,
so the committed cache now survives them.
The reconstructor map is an object literal, so a hostile envelope name
like "constructor" resolved through Object.prototype and returned a
non-Error. Own-key gate plus a fall-through regression.
@socket-security

socket-security Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​qvac/​llm-llamacpp@​0.48.08610010097100

View full report

@yingying0906

Copy link
Copy Markdown
Contributor

Rest all looks good, checked the fixes at 5643433ec.

Only thing left is packages/rag/package.json:77, still @qvac/llm-llamacpp: ^0.47.0 while inference and sdk are on ^0.48.0. A 0.x caret won't cross the minor so rag's tests still run against the addon that consumes n_discarded. It's a devDependency and rag has no source use of the addon so nothing ships wrong, just want to make sure it's intentional.

@iancris
iancris merged commit 25c34e3 into main Sep 2, 2026
34 checks passed
@iancris
iancris deleted the feat/QVAC-24251-sdk-drop-n-discarded branch September 2, 2026 11:38
@donriddo

donriddo commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Only thing left is packages/rag/package.json:77, still @qvac/llm-llamacpp: ^0.47.0

Intentional. I omitted it because a separate PR might be better, raised as #4207.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-e2e-full Triggers full e2e test suite [Currently SDK-only]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants